Author SHA1 Message Date
slhaf 8a5b844a4a feat(partnerctl-init): add release download install option 2026-05-14 19:47:18 +08:00
github-actions[bot] 8d29ea4c9e chore(registry): update latest core release to release-core/0.9.0-preview 2026-05-14 09:08:43 +00:00
github-actions[bot] 4770eaf42f chore(registry): update latest buildable to buildable/0.9.0-preview 2026-05-14 09:07:33 +00:00
slhaf 8bb266a1c3 chore(versioning): bump runtime, framework, and core to 0.9.0-preview 2026-05-14 17:06:06 +08:00
slhaf 9054a9b4ad fix(onebot): correct interaction api dependency version 2026-05-13 13:04:45 +08:00
github-actions[bot] c8d5f577a1 chore: update registry index 2026-05-13 03:35:47 +00:00
slhaf 7c82c4aea5 chore: update onebot registry index version 2026-05-13 11:35:24 +08:00
slhaf 5491ad1747 refactor(versioning): decouple module release versions
- bump parent and external module parent POMs to 1.0.0
- make runtime, interaction API, ctl, and external modules use explicit versions
- centralize internal dependency versions with dedicated properties
- keep framework and core on the shared runtime 0.5.0 line
- prepare partnerctl and module releases for independent versioning
2026-05-13 11:33:34 +08:00
slhaf 1be6ed0198 chore: update idea settings 2026-05-13 10:19:55 +08:00
slhaf 01bfc3ee18 feat(partnerctl-fetch): support fetching raw data via https proxy 2026-05-13 10:16:18 +08:00
slhaf 2d45adf8c3 feat(partnerctl-module): load external modules from remote registry index 2026-05-12 23:34:35 +08:00
slhaf 707fddda79 fix(release): checkout repository before creating ctl release 2026-05-11 18:16:40 +08:00
19 changed files with 342 additions and 54 deletions
+5
View File
@@ -130,6 +130,11 @@ jobs:
echo "Ctl release tag: ${TAG}" echo "Ctl release tag: ${TAG}"
echo "Ctl release version: ${VERSION}" echo "Ctl release version: ${VERSION}"
- name: Checkout release source
uses: actions/checkout@v4
with:
ref: ${{ steps.release.outputs.tag }}
- name: Download release artifacts - name: Download release artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
+19 -18
View File
@@ -1,28 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager"> <component name="EntryPointsManager">
<list size="21"> <list size="22">
<item index="0" class="java.lang.String" itemvalue="lombok.Data" /> <item index="0" class="java.lang.String" itemvalue="lombok.Data" />
<item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" /> <item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" />
<item index="2" class="java.lang.String" itemvalue="picocli.CommandLine.Command" /> <item index="2" class="java.lang.String" itemvalue="picocli.CommandLine.Command" />
<item index="3" class="java.lang.String" itemvalue="picocli.CommandLine.Mixin" /> <item index="3" class="java.lang.String" itemvalue="picocli.CommandLine.Mixin" />
<item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" /> <item index="4" class="java.lang.String" itemvalue="picocli.CommandLine.Option" />
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" /> <item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" /> <item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" /> <item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" /> <item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" /> <item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" /> <item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" /> <item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" /> <item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" /> <item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" /> <item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" /> <item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" /> <item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" /> <item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" /> <item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
<item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" /> <item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
<item index="20" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" /> <item index="20" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
<item index="21" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" />
</list> </list>
<writeAnnotations> <writeAnnotations>
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" /> <writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />
+3 -2
View File
@@ -6,10 +6,11 @@
<parent> <parent>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId> <artifactId>partner</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>partner-core</artifactId> <artifactId>partner-core</artifactId>
<version>0.9.0-preview</version>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -20,7 +21,7 @@
<dependency> <dependency>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner-framework</artifactId> <artifactId>partner-framework</artifactId>
<version>0.5.0</version> <version>${partner.runtime.version}</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api --> <!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api -->
<dependency> <dependency>
@@ -6,22 +6,23 @@
<parent> <parent>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner-external-modules</artifactId> <artifactId>partner-external-modules</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>partner-onebot-adapter</artifactId> <artifactId>partner-onebot-adapter</artifactId>
<version>1.0.0</version>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner-core</artifactId> <artifactId>partner-core</artifactId>
<version>${project.version}</version> <version>${partner.runtime.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner-framework</artifactId> <artifactId>partner-framework</artifactId>
<version>${project.version}</version> <version>${partner.runtime.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
+2 -1
View File
@@ -6,10 +6,11 @@
<parent> <parent>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId> <artifactId>partner</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>partner-external-modules</artifactId> <artifactId>partner-external-modules</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
+3 -2
View File
@@ -6,10 +6,11 @@
<parent> <parent>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId> <artifactId>partner</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>partner-framework</artifactId> <artifactId>partner-framework</artifactId>
<version>0.9.0-preview</version>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -86,7 +87,7 @@
<dependency> <dependency>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner-interaction-api</artifactId> <artifactId>partner-interaction-api</artifactId>
<version>0.5.0</version> <version>${partner.interaction-api.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
+2 -1
View File
@@ -6,10 +6,11 @@
<parent> <parent>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId> <artifactId>partner</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>partner-interaction-api</artifactId> <artifactId>partner-interaction-api</artifactId>
<version>1.0.0</version>
<properties> <properties>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
+3 -2
View File
@@ -6,10 +6,11 @@
<parent> <parent>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId> <artifactId>partner</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
</parent> </parent>
<artifactId>partnerctl</artifactId> <artifactId>partnerctl</artifactId>
<version>1.0.1</version>
<properties> <properties>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
@@ -44,7 +45,7 @@
<dependency> <dependency>
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner-interaction-api</artifactId> <artifactId>partner-interaction-api</artifactId>
<version>0.5.0</version> <version>${partner.interaction-api.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -2,13 +2,12 @@ package work.slhaf.partner.ctl.commands
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import picocli.CommandLine import picocli.CommandLine
import work.slhaf.partner.ctl.commands.InitCommand.InstallChoice.BUILD_FROM_SOURCE
import work.slhaf.partner.ctl.commands.InitCommand.InstallChoice.DOWNLOAD_JAR
import work.slhaf.partner.ctl.commands.data.GatewayConfig import work.slhaf.partner.ctl.commands.data.GatewayConfig
import work.slhaf.partner.ctl.commands.data.OpenAiCompatible import work.slhaf.partner.ctl.commands.data.OpenAiCompatible
import work.slhaf.partner.ctl.commands.data.ProviderConfig import work.slhaf.partner.ctl.commands.data.ProviderConfig
import work.slhaf.partner.ctl.commands.init.buildFromSource import work.slhaf.partner.ctl.commands.init.*
import work.slhaf.partner.ctl.commands.init.configureExternalGateway
import work.slhaf.partner.ctl.commands.init.configureOpenAiCompatible
import work.slhaf.partner.ctl.commands.init.configureWebSocketGateway
import work.slhaf.partner.ctl.i18n.I18n.text import work.slhaf.partner.ctl.i18n.I18n.text
import work.slhaf.partner.ctl.support.CommandInterrupted import work.slhaf.partner.ctl.support.CommandInterrupted
import work.slhaf.partner.ctl.support.inheritCommand import work.slhaf.partner.ctl.support.inheritCommand
@@ -176,11 +175,15 @@ class InitCommand : Runnable {
val installChoice = prompt.select( val installChoice = prompt.select(
label = text("init.install.method.label"), label = text("init.install.method.label"),
choices = listOf(Choice(text("init.install.method.buildFromSource"), InstallChoice.BUILD_FROM_SOURCE)) choices = listOf(
Choice(text("init.install.method.buildFromSource"), BUILD_FROM_SOURCE),
Choice(text("init.install.method.downloadFromRelease"), DOWNLOAD_JAR)
)
) )
when (installChoice) { when (installChoice) {
InstallChoice.BUILD_FROM_SOURCE -> buildFromSource(home, prompt) BUILD_FROM_SOURCE -> buildFromSource(home, prompt)
DOWNLOAD_JAR -> downloadFromRelease(home, prompt)
} }
} }
@@ -348,7 +351,8 @@ class InitCommand : Runnable {
} }
private enum class InstallChoice { private enum class InstallChoice {
BUILD_FROM_SOURCE BUILD_FROM_SOURCE,
DOWNLOAD_JAR
} }
private enum class ModelProviderChoice(val display: String) { private enum class ModelProviderChoice(val display: String) {
@@ -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.buildCommand") to manifest.source.buildCommand.joinToString(" "),
text("configure.gateway.external.details.artifact") to "${manifest.source.artifactDirectory}/${manifest.source.artifactPattern}", 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.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? { private fun validateFieldValue(field: Field, value: String): String? {
if (value.isBlank() && !field.required) return null if (value.isBlank() && !field.required) return null
return when (field.type) { return when (field.type) {
FieldType.STRING -> null FieldType.STRING -> null
FieldType.INT -> value.toIntOrNull()?.let { null } ?: text("configure.field.error.int", field.label) FieldType.INT -> {
FieldType.NUMBER -> value.toDoubleOrNull()?.let { null } ?: text("configure.field.error.number", field.label) if (value.toIntOrNull() == null) {
FieldType.BOOLEAN -> value.toBooleanStrictOrNull()?.let { null } ?: text("configure.field.error.boolean", field.label) text("configure.field.error.int", field.label)
FieldType.RAW_JSON -> runCatching { Json.parseToJsonElement(value) } } else {
.exceptionOrNull() null
?.let { text("configure.field.error.rawJson", field.label) } }
}
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
}
}
} }
} }
@@ -1,15 +1,19 @@
package work.slhaf.partner.ctl.commands.init package work.slhaf.partner.ctl.commands.init
import work.slhaf.partner.ctl.i18n.I18n.text
import work.slhaf.partner.ctl.support.SourceBuildInstallSpec import work.slhaf.partner.ctl.support.SourceBuildInstallSpec
import work.slhaf.partner.ctl.support.buildAndInstallFromSource import work.slhaf.partner.ctl.support.buildAndInstallFromSource
import work.slhaf.partner.ctl.support.downloadTo
import work.slhaf.partner.ctl.support.registryIndex
import work.slhaf.partner.ctl.ui.Prompt import work.slhaf.partner.ctl.ui.Prompt
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.exists
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.name 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) { fun buildFromSource(home: Path, prompt: Prompt) {
buildAndInstallFromSource( buildAndInstallFromSource(
@@ -40,3 +44,41 @@ private fun findLargestJar(directory: Path): Path? {
.orElse(null) .orElse(null)
} }
} }
fun downloadFromRelease(home: Path, prompt: Prompt) {
prompt.info(text("init.install.method.downloadFromRelease.startDownloading"))
val path = home.resolve("resources/partner-core.jar").toAbsolutePath().normalize()
downloadTo(registryIndex.partner.latestRelease.url, path) { downloaded, total ->
if (total != null && total > 0) {
val percent = downloaded * 100 / total
updateLine(
text(
"init.install.method.downloadFromRelease.progress.percent",
percent
)
)
} else {
updateLine(
text(
"init.install.method.downloadFromRelease.progress.size",
downloaded / 1024
)
)
}
}
finishLine(text("init.install.method.downloadFromRelease.done"))
if (!path.exists()) {
throw IllegalStateException("Unable to find downloaded partner release at $path")
}
prompt.success(text("init.install.method.downloadFromRelease.success"))
}
fun updateLine(text: String) {
print("\r\u001B[2K$text")
System.out.flush()
}
fun finishLine(text: String) {
updateLine(text)
println()
}
@@ -1,10 +1,20 @@
package work.slhaf.partner.ctl.support package work.slhaf.partner.ctl.support
import kotlinx.serialization.Serializable 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"
val registryIndex = run {
Json.decodeFromString<RegistryIndex>(fetchText(indexUrl))
}
private fun loadModules(): Set<ModuleManifest> { private fun loadModules(): Set<ModuleManifest> {
// TODO: 待实现具体加载逻辑 return registryIndex.externalModules.map { indexItem ->
return emptySet() val manifestStr = fetchText("$registryUrl/${indexItem.registryRef}")
return@map Json.decodeFromString<ModuleManifest>(manifestStr)
}.toSet()
} }
fun loadAvailableGateway(): Set<ModuleManifest> { fun loadAvailableGateway(): Set<ModuleManifest> {
@@ -91,4 +101,36 @@ enum class FieldType {
NUMBER, NUMBER,
BOOLEAN, BOOLEAN,
RAW_JSON, 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
)
@@ -0,0 +1,135 @@
package work.slhaf.partner.ctl.support
import java.io.IOException
import java.net.InetSocketAddress
import java.net.ProxySelector
import java.net.URI
import java.net.http.*
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.time.Duration
import kotlin.io.path.isDirectory
private val httpClient: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(20))
.followRedirects(HttpClient.Redirect.NORMAL)
.apply {
proxySelectorFromEnv()?.let(::proxy)
}
.build()
private fun proxySelectorFromEnv(): ProxySelector? {
val proxyText = System.getenv("HTTPS_PROXY")
?: System.getenv("https_proxy")
?: return null
val proxyUri = URI.create(proxyText)
val host = proxyUri.host
?: throw IllegalArgumentException("Invalid HTTPS_PROXY host: $proxyText")
val port = proxyUri.port
if (port == -1) {
throw IllegalArgumentException("HTTPS_PROXY must include port: $proxyText")
}
return ProxySelector.of(InetSocketAddress(host, port))
}
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)
}
fun downloadTo(
url: String,
targetPath: Path,
onProgress: (downloaded: Long, total: Long?) -> Unit = { _, _ -> }
) {
if (targetPath.isDirectory()) {
throw IllegalArgumentException("Target path must be a file")
}
val targetPath = targetPath.toAbsolutePath().normalize()
val targetFile = targetPath.toFile()
val temp = Files.createTempFile(
"${targetFile.name}-${System.currentTimeMillis()}", ".${targetFile.extension}.download"
)
try {
val request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build()
val response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofInputStream()
)
if (response.statusCode() !in 200..299) {
throw IllegalStateException("Failed to download from $url: HTTP ${response.statusCode()}")
}
val totalBytes = response.headers()
.firstValue("Content-Length")
.orElse(null)
?.toLongOrNull()
response.body().use { input ->
Files.newOutputStream(temp).use { output ->
val buffer = ByteArray(8192)
var downloaded = 0L
while (true) {
val read = input.read(buffer)
if (read < 0) break
output.write(buffer, 0, read)
downloaded += read
onProgress(downloaded, totalBytes)
}
}
}
Files.move(temp, targetPath, StandardCopyOption.REPLACE_EXISTING)
} catch (e: Exception) {
Files.deleteIfExists(temp)
throw e
}
}
@@ -31,6 +31,12 @@ init.home.overwrite.refuseBroadDirectory=Refuse to overwrite suspiciously broad
init.install.section=Install Partner init.install.section=Install Partner
init.install.method.label=Choose an installation method init.install.method.label=Choose an installation method
init.install.method.buildFromSource=Build Partner from source init.install.method.buildFromSource=Build Partner from source
init.install.method.downloadFromRelease=Download Partner release
init.install.method.downloadFromRelease.startDownloading=Downloading Partner release...
init.install.method.downloadFromRelease.success=Partner release downloaded successfully.
init.install.method.downloadFromRelease.progress.percent=Downloading Partner release... {0}%
init.install.method.downloadFromRelease.progress.size=Downloading Partner release... {0} KB
init.install.method.downloadFromRelease.done=Downloading Partner release... Done
init.gateway.section=Configure Gateway init.gateway.section=Configure Gateway
init.gateway.select.label=Select gateway init.gateway.select.label=Select gateway
init.gateway.websocket.choice=WebSocket Gateway init.gateway.websocket.choice=WebSocket Gateway
@@ -31,6 +31,12 @@ init.home.overwrite.refuseBroadDirectory=拒绝覆盖范围过大的目录:{0}
init.install.section=安装 Partner init.install.section=安装 Partner
init.install.method.label=选择安装方式 init.install.method.label=选择安装方式
init.install.method.buildFromSource=从源码构建 Partner init.install.method.buildFromSource=从源码构建 Partner
init.install.method.downloadFromRelease=下载 Partner 发布包
init.install.method.downloadFromRelease.startDownloading=正在下载 Partner 发布包...
init.install.method.downloadFromRelease.success=Partner 发布包下载完成。
init.install.method.downloadFromRelease.progress.percent=正在下载 Partner 发布包... {0}%
init.install.method.downloadFromRelease.progress.size=正在下载 Partner 发布包... {0} KB
init.install.method.downloadFromRelease.done=正在下载 Partner 发布包... 完成
init.gateway.section=配置网关 init.gateway.section=配置网关
init.gateway.select.label=选择网关 init.gateway.select.label=选择网关
init.gateway.websocket.choice=WebSocket Gateway init.gateway.websocket.choice=WebSocket Gateway
@@ -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)
}
+4 -1
View File
@@ -5,7 +5,7 @@
<groupId>work.slhaf.partner</groupId> <groupId>work.slhaf.partner</groupId>
<artifactId>partner</artifactId> <artifactId>partner</artifactId>
<version>0.5.0</version> <version>1.0.0</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
@@ -26,6 +26,9 @@
<!-- 推荐仓库默认不跳测试;本地需要时再 -DskipTests=true --> <!-- 推荐仓库默认不跳测试;本地需要时再 -DskipTests=true -->
<skipTests>false</skipTests> <skipTests>false</skipTests>
<partner.runtime.version>0.9.0-preview</partner.runtime.version>
<partner.interaction-api.version>1.0.0</partner.interaction-api.version>
</properties> </properties>
<dependencies> <dependencies>
+4 -4
View File
@@ -2,17 +2,17 @@
"partner": { "partner": {
"latestBuildable": { "latestBuildable": {
"url": "https://github.com/slhaf/Partner.git", "url": "https://github.com/slhaf/Partner.git",
"ref": "buildable/0.5.0" "ref": "buildable/0.9.0-preview"
}, },
"latestRelease": { "latestRelease": {
"url": "https://github.com/slhaf/Partner/releases/download/release-core%2F0.5.0/partner-core-0.5.0.jar", "url": "https://github.com/slhaf/Partner/releases/download/release-core%2F0.9.0-preview/partner-core-0.9.0-preview.jar",
"version": "0.5.0" "version": "0.9.0-preview"
} }
}, },
"externalModules": [ "externalModules": [
{ {
"name": "OneBot Adapter", "name": "OneBot Adapter",
"version": "0.5.0", "version": "1.0.0",
"withGateway": true, "withGateway": true,
"registryRef": "modules/onebot-adapter.json" "registryRef": "modules/onebot-adapter.json"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "onebot_channel", "id": "onebot_channel",
"name": "OneBot Adapter", "name": "OneBot Adapter",
"version": "0.5.0", "version": "1.0.0",
"withGateway": true, "withGateway": true,
"description": "OneBot v11 reverse WebSocket gateway adapter for Partner. It accepts reverse WebSocket connections from a OneBot implementation and converts private message events into Partner input events.", "description": "OneBot v11 reverse WebSocket gateway adapter for Partner. It accepts reverse WebSocket connections from a OneBot implementation and converts private message events into Partner input events.",
"source": { "source": {