mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
feat(partnerctl-init): add i18n message bundles for init flow and source build output
This commit is contained in:
@@ -115,6 +115,7 @@
|
|||||||
<buildArg>--no-fallback</buildArg>
|
<buildArg>--no-fallback</buildArg>
|
||||||
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
|
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
|
||||||
<buildArg>--initialize-at-build-time=kotlin.DeprecationLevel</buildArg>
|
<buildArg>--initialize-at-build-time=kotlin.DeprecationLevel</buildArg>
|
||||||
|
<buildArg>-H:IncludeResourceBundles=i18n.messages</buildArg>
|
||||||
</buildArgs>
|
</buildArgs>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import work.slhaf.partner.ctl.commands.init.buildFromSource
|
|||||||
import work.slhaf.partner.ctl.commands.init.configureExternalGateway
|
import work.slhaf.partner.ctl.commands.init.configureExternalGateway
|
||||||
import work.slhaf.partner.ctl.commands.init.configureOpenAiCompatible
|
import work.slhaf.partner.ctl.commands.init.configureOpenAiCompatible
|
||||||
import work.slhaf.partner.ctl.commands.init.configureWebSocketGateway
|
import work.slhaf.partner.ctl.commands.init.configureWebSocketGateway
|
||||||
|
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
|
||||||
import work.slhaf.partner.ctl.support.loadAvailableGateway
|
import work.slhaf.partner.ctl.support.loadAvailableGateway
|
||||||
@@ -52,7 +53,7 @@ class InitCommand : Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initHome(prompt: Prompt) {
|
private fun initHome(prompt: Prompt) {
|
||||||
prompt.section("Initialize Partner Home")
|
prompt.section(text("init.home.section"))
|
||||||
|
|
||||||
home = choosePartnerHome(prompt)
|
home = choosePartnerHome(prompt)
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ class InitCommand : Runnable {
|
|||||||
Files.createDirectories(home.resolve("resource"))
|
Files.createDirectories(home.resolve("resource"))
|
||||||
Files.createDirectories(home.resolve("config"))
|
Files.createDirectories(home.resolve("config"))
|
||||||
|
|
||||||
prompt.success("Partner Home initialized at $home")
|
prompt.success(text("init.home.success", home))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun choosePartnerHome(prompt: Prompt): Path {
|
private fun choosePartnerHome(prompt: Prompt): Path {
|
||||||
@@ -68,7 +69,7 @@ class InitCommand : Runnable {
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val selectedHome = prompt.askPath(
|
val selectedHome = prompt.askPath(
|
||||||
label = "Partner Home",
|
label = text("init.home.label"),
|
||||||
defaultValue = defaultHome,
|
defaultValue = defaultHome,
|
||||||
required = true,
|
required = true,
|
||||||
directoryOnly = true,
|
directoryOnly = true,
|
||||||
@@ -78,29 +79,29 @@ class InitCommand : Runnable {
|
|||||||
return selectedHome
|
return selectedHome
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt.warn("Partner Home already contains files: $selectedHome")
|
prompt.warn(text("init.home.duplicate.warning", selectedHome))
|
||||||
|
|
||||||
when (prompt.select(
|
when (prompt.select(
|
||||||
label = "Partner Home already contains files. Choose how to continue",
|
label = text("init.home.duplicate.choice.label"),
|
||||||
choices = listOf(
|
choices = listOf(
|
||||||
Choice(
|
Choice(
|
||||||
"Use another Partner Home",
|
text("init.home.duplicate.choice.another"),
|
||||||
HomeDuplicateChoice.ANOTHER,
|
HomeDuplicateChoice.ANOTHER,
|
||||||
"Choose a different directory",
|
text("init.home.duplicate.choice.another.description"),
|
||||||
),
|
),
|
||||||
Choice(
|
Choice(
|
||||||
"Overwrite current Partner Home",
|
text("init.home.duplicate.choice.overwrite"),
|
||||||
HomeDuplicateChoice.OVERWRITE,
|
HomeDuplicateChoice.OVERWRITE,
|
||||||
"Delete existing contents and continue",
|
text("init.home.duplicate.choice.overwrite.description"),
|
||||||
),
|
),
|
||||||
Choice("Cancel init", HomeDuplicateChoice.EXIT),
|
Choice(text("init.home.duplicate.choice.exit"), HomeDuplicateChoice.EXIT),
|
||||||
),
|
),
|
||||||
defaultIndex = 0,
|
defaultIndex = 0,
|
||||||
)) {
|
)) {
|
||||||
HomeDuplicateChoice.ANOTHER -> continue
|
HomeDuplicateChoice.ANOTHER -> continue
|
||||||
HomeDuplicateChoice.OVERWRITE -> {
|
HomeDuplicateChoice.OVERWRITE -> {
|
||||||
validateSafeHomeOverwrite(selectedHome)
|
validateSafeHomeOverwrite(selectedHome)
|
||||||
if (!prompt.confirm("Delete all files under $selectedHome?", false)) {
|
if (!prompt.confirm(text("init.home.duplicate.confirmDelete", selectedHome), false)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
clearHomeDirectory(selectedHome)
|
clearHomeDirectory(selectedHome)
|
||||||
@@ -134,7 +135,7 @@ class InitCommand : Runnable {
|
|||||||
private fun clearHomeDirectory(path: Path) {
|
private fun clearHomeDirectory(path: Path) {
|
||||||
if (!Files.exists(path)) return
|
if (!Files.exists(path)) return
|
||||||
if (!Files.isDirectory(path)) {
|
if (!Files.isDirectory(path)) {
|
||||||
throw CommandInterrupted("Partner Home is not a directory: $path")
|
throw CommandInterrupted(text("init.home.notDirectory", path))
|
||||||
}
|
}
|
||||||
|
|
||||||
Files.walk(path).use { stream ->
|
Files.walk(path).use { stream ->
|
||||||
@@ -150,25 +151,25 @@ class InitCommand : Runnable {
|
|||||||
val userHome = Paths.get(System.getProperty("user.home")).toAbsolutePath().normalize()
|
val userHome = Paths.get(System.getProperty("user.home")).toAbsolutePath().normalize()
|
||||||
|
|
||||||
if (normalized == normalized.root) {
|
if (normalized == normalized.root) {
|
||||||
throw CommandInterrupted("Refuse to overwrite filesystem root: $normalized")
|
throw CommandInterrupted(text("init.home.overwrite.refuseRoot", normalized))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized == userHome) {
|
if (normalized == userHome) {
|
||||||
throw CommandInterrupted("Refuse to overwrite user home directory: $normalized")
|
throw CommandInterrupted(text("init.home.overwrite.refuseUserHome", normalized))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.nameCount < 2) {
|
if (normalized.nameCount < 2) {
|
||||||
throw CommandInterrupted("Refuse to overwrite suspiciously broad directory: $normalized")
|
throw CommandInterrupted(text("init.home.overwrite.refuseBroadDirectory", normalized))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installPartner(prompt: Prompt) {
|
private fun installPartner(prompt: Prompt) {
|
||||||
|
|
||||||
prompt.section("Install Partner")
|
prompt.section(text("init.install.section"))
|
||||||
|
|
||||||
val installChoice = prompt.select(
|
val installChoice = prompt.select(
|
||||||
label = "Choose a installation method",
|
label = text("init.install.method.label"),
|
||||||
choices = listOf(Choice("Build Partner from source", InstallChoice.BUILD_FROM_SOURCE))
|
choices = listOf(Choice(text("init.install.method.buildFromSource"), InstallChoice.BUILD_FROM_SOURCE))
|
||||||
)
|
)
|
||||||
|
|
||||||
when (installChoice) {
|
when (installChoice) {
|
||||||
@@ -178,12 +179,12 @@ class InitCommand : Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun configureGateway(prompt: Prompt) {
|
private fun configureGateway(prompt: Prompt) {
|
||||||
prompt.section("Configure Gateway")
|
prompt.section(text("init.gateway.section"))
|
||||||
|
|
||||||
val providedGateways = loadAvailableGateway()
|
val providedGateways = loadAvailableGateway()
|
||||||
val selectedGateways = prompt.multiSelect(
|
val selectedGateways = prompt.multiSelect(
|
||||||
label = "Select gateway",
|
label = text("init.gateway.select.label"),
|
||||||
choices = listOf(Choice("WebSocket Gateway", "websocket_channel")) +
|
choices = listOf(Choice(text("init.gateway.websocket.choice"), "websocket_channel")) +
|
||||||
providedGateways.map {
|
providedGateways.map {
|
||||||
Choice(it.name, it.id)
|
Choice(it.name, it.id)
|
||||||
}
|
}
|
||||||
@@ -198,29 +199,29 @@ class InitCommand : Runnable {
|
|||||||
if (manifest != null) {
|
if (manifest != null) {
|
||||||
return@map configureExternalGateway(home, prompt, manifest)
|
return@map configureExternalGateway(home, prompt, manifest)
|
||||||
} else {
|
} else {
|
||||||
prompt.warn("Could not find gateway with id $gateway")
|
prompt.warn(text("init.gateway.warn.notFound", gateway))
|
||||||
return@map null
|
return@map null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: PromptCancelledException) {
|
} catch (_: PromptCancelledException) {
|
||||||
prompt.warn("Gateway: $gateway configuration skipped")
|
prompt.warn(text("init.gateway.warn.skipped", gateway))
|
||||||
return@map null
|
return@map null
|
||||||
}
|
}
|
||||||
}.filterNotNull()
|
}.filterNotNull()
|
||||||
|
|
||||||
val defaultChannel = if (configuredChannels.isEmpty()) {
|
val defaultChannel = if (configuredChannels.isEmpty()) {
|
||||||
prompt.info("Skipped gateway configuration. Partner will use WebSocket as default gateway")
|
prompt.info(text("init.gateway.info.skippedUseDefault"))
|
||||||
return
|
return
|
||||||
} else if (configuredChannels.size == 1) {
|
} else if (configuredChannels.size == 1) {
|
||||||
configuredChannels.first().channelName
|
configuredChannels.first().channelName
|
||||||
} else {
|
} else {
|
||||||
prompt.select(
|
prompt.select(
|
||||||
label = "Set default channel",
|
label = text("init.gateway.defaultChannel.label"),
|
||||||
choices = configuredChannels.map { Choice(it.channelName, it.channelName) }
|
choices = configuredChannels.map { Choice(it.channelName, it.channelName) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt.info("The default channel will be set to $defaultChannel")
|
prompt.info(text("init.gateway.info.defaultChannel", defaultChannel))
|
||||||
|
|
||||||
val gatewayConfig = GatewayConfig(
|
val gatewayConfig = GatewayConfig(
|
||||||
defaultChannel = defaultChannel,
|
defaultChannel = defaultChannel,
|
||||||
@@ -236,11 +237,11 @@ class InitCommand : Runnable {
|
|||||||
val gatewayPath = home.resolve("config").resolve("gateway.json").toAbsolutePath().normalize()
|
val gatewayPath = home.resolve("config").resolve("gateway.json").toAbsolutePath().normalize()
|
||||||
Files.writeString(gatewayPath, gatewayStr)
|
Files.writeString(gatewayPath, gatewayStr)
|
||||||
|
|
||||||
prompt.success("Gateway config written to $gatewayPath")
|
prompt.success(text("init.gateway.success.configWritten", gatewayPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureModel(prompt: Prompt) {
|
private fun configureModel(prompt: Prompt) {
|
||||||
prompt.section("Configure Model")
|
prompt.section(text("init.model.section"))
|
||||||
|
|
||||||
val modelChoices = ModelProviderChoice.entries.map { Choice(it.display, it) }
|
val modelChoices = ModelProviderChoice.entries.map { Choice(it.display, it) }
|
||||||
|
|
||||||
@@ -250,9 +251,9 @@ class InitCommand : Runnable {
|
|||||||
while (true) {
|
while (true) {
|
||||||
val choice = prompt.select(
|
val choice = prompt.select(
|
||||||
label = if (!defaultAlreadySet) {
|
label = if (!defaultAlreadySet) {
|
||||||
"Choose default model provider type"
|
text("init.model.provider.default.label")
|
||||||
} else {
|
} else {
|
||||||
"Choose model provider type"
|
text("init.model.provider.label")
|
||||||
},
|
},
|
||||||
choices = modelChoices
|
choices = modelChoices
|
||||||
)
|
)
|
||||||
@@ -264,10 +265,9 @@ class InitCommand : Runnable {
|
|||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
prompt.warn(
|
prompt.warn(
|
||||||
"No default model provider configured. Partner may not start normally unless model.json exists " +
|
text("init.model.warn.noDefaultProvider")
|
||||||
"or PARTNER_DEFAULT_BASE_URL, PARTNER_DEFAULT_API_KEY, and PARTNER_DEFAULT_MODEL are provided at runtime."
|
|
||||||
)
|
)
|
||||||
if (prompt.confirm("Skip model configuration?", false)) {
|
if (prompt.confirm(text("init.model.confirm.skipConfiguration"), false)) {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -280,7 +280,7 @@ class InitCommand : Runnable {
|
|||||||
chosenModelProviders.add(it)
|
chosenModelProviders.add(it)
|
||||||
if (!defaultAlreadySet) {
|
if (!defaultAlreadySet) {
|
||||||
defaultAlreadySet = true
|
defaultAlreadySet = true
|
||||||
if (!prompt.confirm("Add additional model provider?", false)) {
|
if (!prompt.confirm(text("init.model.confirm.addAdditionalProvider"), false)) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,15 +306,15 @@ class InitCommand : Runnable {
|
|||||||
val modelPath = home.resolve("config").resolve("model.json").toAbsolutePath().normalize()
|
val modelPath = home.resolve("config").resolve("model.json").toAbsolutePath().normalize()
|
||||||
Files.writeString(modelPath, json.encodeToString(JsonObject.serializer(), jsonObject))
|
Files.writeString(modelPath, json.encodeToString(JsonObject.serializer(), jsonObject))
|
||||||
|
|
||||||
prompt.success("Model config written to $modelPath")
|
prompt.success(text("init.model.success.configWritten", modelPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finalize(prompt: Prompt) {
|
private fun finalize(prompt: Prompt) {
|
||||||
prompt.section("Finish")
|
prompt.section(text("init.finish.section"))
|
||||||
|
|
||||||
if (!prompt.confirm("Start Partner now?", false)) {
|
if (!prompt.confirm(text("init.finish.confirm.startNow"), false)) {
|
||||||
prompt.info("Initialization completed.")
|
prompt.info(text("init.finish.info.completed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ class InitCommand : Runnable {
|
|||||||
throw CommandInterrupted("Partner runtime jar does not exist: $partnerJar")
|
throw CommandInterrupted("Partner runtime jar does not exist: $partnerJar")
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt.info("Starting Partner...")
|
prompt.info(text("init.finish.info.starting"))
|
||||||
val exitCode = inheritCommand(
|
val exitCode = inheritCommand(
|
||||||
command = listOf("java", "-jar", partnerJar.toString()),
|
command = listOf("java", "-jar", partnerJar.toString()),
|
||||||
environment = mapOf("PARTNER_HOME" to home.toString()),
|
environment = mapOf("PARTNER_HOME" to home.toString()),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.json.*
|
|||||||
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.i18n.I18n.text
|
||||||
import work.slhaf.partner.ctl.support.*
|
import work.slhaf.partner.ctl.support.*
|
||||||
import work.slhaf.partner.ctl.ui.Prompt
|
import work.slhaf.partner.ctl.ui.Prompt
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -13,26 +14,26 @@ import java.nio.file.Paths
|
|||||||
import kotlin.io.path.isDirectory
|
import kotlin.io.path.isDirectory
|
||||||
|
|
||||||
fun configureWebSocketGateway(prompt: Prompt): GatewayConfig.ChannelConfig {
|
fun configureWebSocketGateway(prompt: Prompt): GatewayConfig.ChannelConfig {
|
||||||
prompt.section("Configure Gateway: WebSocket Gateway")
|
prompt.section(text("configure.gateway.websocket.section"))
|
||||||
|
|
||||||
val port = prompt.ask("port", "29600") {
|
val port = prompt.ask(text("configure.gateway.websocket.port.label"), "29600") {
|
||||||
val intValue = it.toIntOrNull() ?: return@ask "WebSocket port only accepts int value"
|
val intValue = it.toIntOrNull() ?: return@ask text("configure.gateway.websocket.port.error.int")
|
||||||
if (intValue !in 1..65565) "WebSocket port should be between 1 and 65565" else null
|
if (intValue !in 1..65565) text("configure.gateway.websocket.port.error.range") else null
|
||||||
}.toInt()
|
}.toInt()
|
||||||
|
|
||||||
val heartbeatInterval = prompt.ask("heartbeat interval", "10000") {
|
val heartbeatInterval = prompt.ask(text("configure.gateway.websocket.heartbeatInterval.label"), "10000") {
|
||||||
it.toLongOrNull() ?: return@ask "Heartbeat interval only accepts long value"
|
it.toLongOrNull() ?: return@ask text("configure.gateway.websocket.heartbeatInterval.error.long")
|
||||||
return@ask null
|
return@ask null
|
||||||
}.toLong()
|
}.toLong()
|
||||||
|
|
||||||
val hostname = prompt.ask("Listening hostname", "127.0.0.1") {
|
val hostname = prompt.ask(text("configure.gateway.websocket.hostname.label"), "127.0.0.1") {
|
||||||
val host = it.trim()
|
val host = it.trim()
|
||||||
return@ask when {
|
return@ask when {
|
||||||
host.isEmpty() -> "Hostname is required"
|
host.isEmpty() -> text("configure.gateway.websocket.hostname.error.required")
|
||||||
host.contains(Regex("\\s")) -> "Hostname must not contain whitespace."
|
host.contains(Regex("\\s")) -> text("configure.gateway.websocket.hostname.error.whitespace")
|
||||||
host.contains("://") -> "Do not include protocol. Use hostname only, for example: 127.0.0.1"
|
host.contains("://") -> text("configure.gateway.websocket.hostname.error.protocol")
|
||||||
host.contains("/") -> "Do not include path. Use hostname only."
|
host.contains("/") -> text("configure.gateway.websocket.hostname.error.path")
|
||||||
looksLikeHostWithPort(host) -> "Do not include port here. Port is configured separately."
|
looksLikeHostWithPort(host) -> text("configure.gateway.websocket.hostname.error.port")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,21 +54,21 @@ private fun looksLikeHostWithPort(value: String): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun configureExternalGateway(home: Path, prompt: Prompt, manifest: ModuleManifest): GatewayConfig.ChannelConfig? {
|
fun configureExternalGateway(home: Path, prompt: Prompt, manifest: ModuleManifest): GatewayConfig.ChannelConfig? {
|
||||||
prompt.section("Configure Gateway: ${manifest.name}")
|
prompt.section(text("configure.gateway.external.section", manifest.name))
|
||||||
|
|
||||||
prompt.details(
|
prompt.details(
|
||||||
title = "Gateway module details",
|
title = text("configure.gateway.external.details.title"),
|
||||||
items = listOf(
|
items = listOf(
|
||||||
"Description" to manifest.description,
|
text("configure.gateway.external.details.description") to manifest.description,
|
||||||
"Source" to manifest.source.url,
|
text("configure.gateway.external.details.source") to manifest.source.url,
|
||||||
"Build command" to manifest.source.buildCommand.joinToString(" "),
|
text("configure.gateway.external.details.buildCommand") to manifest.source.buildCommand.joinToString(" "),
|
||||||
"Artifact" to "${manifest.source.artifactDirectory}/${manifest.source.artifactPattern}",
|
text("configure.gateway.external.details.artifact") to "${manifest.source.artifactDirectory}/${manifest.source.artifactPattern}",
|
||||||
"Install target" to manifest.install.target,
|
text("configure.gateway.external.details.installTarget") to manifest.install.target,
|
||||||
"Config target" to (manifest.config?.target ?: "No config"),
|
text("configure.gateway.external.details.configTarget") to (manifest.config?.target ?: text("configure.gateway.external.details.noConfig")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!prompt.confirm("Continue installation?", true)) {
|
if (!prompt.confirm(text("configure.gateway.external.confirmContinue"), true)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,20 +144,20 @@ private fun validateFieldValue(field: Field, value: String): String? {
|
|||||||
|
|
||||||
return when (field.type) {
|
return when (field.type) {
|
||||||
FieldType.STRING -> null
|
FieldType.STRING -> null
|
||||||
FieldType.INT -> value.toIntOrNull()?.let { null } ?: "${field.label} only accepts int value"
|
FieldType.INT -> value.toIntOrNull()?.let { null } ?: text("configure.field.error.int", field.label)
|
||||||
FieldType.NUMBER -> value.toDoubleOrNull()?.let { null } ?: "${field.label} only accepts number value"
|
FieldType.NUMBER -> value.toDoubleOrNull()?.let { null } ?: text("configure.field.error.number", field.label)
|
||||||
FieldType.BOOLEAN -> value.toBooleanStrictOrNull()?.let { null } ?: "${field.label} only accepts true or false"
|
FieldType.BOOLEAN -> value.toBooleanStrictOrNull()?.let { null } ?: text("configure.field.error.boolean", field.label)
|
||||||
FieldType.RAW_JSON -> runCatching { Json.parseToJsonElement(value) }
|
FieldType.RAW_JSON -> runCatching { Json.parseToJsonElement(value) }
|
||||||
.exceptionOrNull()
|
.exceptionOrNull()
|
||||||
?.let { "${field.label} only accepts valid JSON" }
|
?.let { text("configure.field.error.rawJson", field.label) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): ProviderConfig {
|
fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): ProviderConfig {
|
||||||
val name = if (defaultAlreadySet) {
|
val name = if (defaultAlreadySet) {
|
||||||
prompt.ask("Provider name") {
|
prompt.ask(text("configure.model.openAiCompatible.providerName.label")) {
|
||||||
if (it == "default") {
|
if (it == "default") {
|
||||||
"Default provider cannot be duplicate"
|
text("configure.model.openAiCompatible.providerName.error.duplicateDefault")
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -165,12 +166,12 @@ fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): Provi
|
|||||||
"default"
|
"default"
|
||||||
}
|
}
|
||||||
|
|
||||||
val baseUrl = prompt.ask("Base url") { value ->
|
val baseUrl = prompt.ask(text("configure.model.openAiCompatible.baseUrl.label")) { value ->
|
||||||
validateNetworkUrl(value)
|
validateNetworkUrl(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
val apikey = prompt.ask("Apikey")
|
val apikey = prompt.ask(text("configure.model.openAiCompatible.apiKey.label"))
|
||||||
val defaultModel = prompt.ask("Default model")
|
val defaultModel = prompt.ask(text("configure.model.openAiCompatible.defaultModel.label"))
|
||||||
return OpenAiCompatible(
|
return OpenAiCompatible(
|
||||||
name = name,
|
name = name,
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
@@ -182,18 +183,18 @@ fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): Provi
|
|||||||
private fun validateNetworkUrl(value: String): String? {
|
private fun validateNetworkUrl(value: String): String? {
|
||||||
val trimmed = value.trim()
|
val trimmed = value.trim()
|
||||||
if (trimmed.isEmpty()) {
|
if (trimmed.isEmpty()) {
|
||||||
return "Base url is required"
|
return text("configure.model.openAiCompatible.baseUrl.error.required")
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = runCatching { URI(trimmed) }.getOrElse {
|
val uri = runCatching { URI(trimmed) }.getOrElse {
|
||||||
return "Base url must be a valid URL"
|
return text("configure.model.openAiCompatible.baseUrl.error.validUrl")
|
||||||
}
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
uri.scheme !in setOf("http", "https") -> "Base url must start with http:// or https://"
|
uri.scheme !in setOf("http", "https") -> text("configure.model.openAiCompatible.baseUrl.error.scheme")
|
||||||
uri.host.isNullOrBlank() -> "Base url must include a valid host"
|
uri.host.isNullOrBlank() -> text("configure.model.openAiCompatible.baseUrl.error.host")
|
||||||
uri.rawUserInfo != null -> "Base url must not include user info"
|
uri.rawUserInfo != null -> text("configure.model.openAiCompatible.baseUrl.error.userInfo")
|
||||||
uri.rawFragment != null -> "Base url must not include fragment"
|
uri.rawFragment != null -> text("configure.model.openAiCompatible.baseUrl.error.fragment")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
PartnerCtl/src/main/java/work/slhaf/partner/ctl/i18n/I18n.kt
Normal file
20
PartnerCtl/src/main/java/work/slhaf/partner/ctl/i18n/I18n.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package work.slhaf.partner.ctl.i18n
|
||||||
|
|
||||||
|
import java.text.MessageFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object I18n {
|
||||||
|
private const val BUNDLE = "i18n.messages"
|
||||||
|
|
||||||
|
private val locale: Locale by lazy {
|
||||||
|
System.getenv("PARTNER_LOCALE")
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { Locale.forLanguageTag(it.replace('_', '-')) }
|
||||||
|
?: Locale.getDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun text(key: String, vararg args: Any?): String {
|
||||||
|
val pattern = ResourceBundle.getBundle(BUNDLE, locale).getString(key)
|
||||||
|
return MessageFormat.format(pattern, *args)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package work.slhaf.partner.ctl.support
|
package work.slhaf.partner.ctl.support
|
||||||
|
|
||||||
|
import work.slhaf.partner.ctl.i18n.I18n.text
|
||||||
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
|
||||||
@@ -51,30 +52,30 @@ fun buildAndInstallFromSource(
|
|||||||
val targetPath = home.resolve(spec.installRelativePath)
|
val targetPath = home.resolve(spec.installRelativePath)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
prompt.info("Cloning ${spec.displayName} source from ${spec.repoUrl}")
|
prompt.info(text("sourceBuild.info.cloning", spec.displayName, spec.repoUrl))
|
||||||
val cloneExitCode = inheritCommand(
|
val cloneExitCode = inheritCommand(
|
||||||
command = listOf("git", "clone", "--depth", "1", spec.repoUrl, sourceDir.toString()),
|
command = listOf("git", "clone", "--depth", "1", spec.repoUrl, sourceDir.toString()),
|
||||||
)
|
)
|
||||||
if (cloneExitCode != 0) {
|
if (cloneExitCode != 0) {
|
||||||
throw CommandInterrupted("Failed to clone ${spec.displayName} source from ${spec.repoUrl}")
|
throw CommandInterrupted(text("sourceBuild.error.cloneFailed", spec.displayName, spec.repoUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt.info("Building ${spec.displayName}")
|
prompt.info(text("sourceBuild.info.building", spec.displayName))
|
||||||
val buildExitCode = inheritCommand(
|
val buildExitCode = inheritCommand(
|
||||||
command = spec.buildCommand,
|
command = spec.buildCommand,
|
||||||
workingDirectory = sourceDir,
|
workingDirectory = sourceDir,
|
||||||
)
|
)
|
||||||
if (buildExitCode != 0) {
|
if (buildExitCode != 0) {
|
||||||
throw CommandInterrupted("Failed to build ${spec.displayName}.")
|
throw CommandInterrupted(text("sourceBuild.error.buildFailed", spec.displayName))
|
||||||
}
|
}
|
||||||
|
|
||||||
val artifactDir = sourceDir.resolve(spec.artifactDirectory)
|
val artifactDir = sourceDir.resolve(spec.artifactDirectory)
|
||||||
val artifact = spec.artifactSelector(artifactDir)
|
val artifact = spec.artifactSelector(artifactDir)
|
||||||
?: throw CommandInterrupted("Could not find built ${spec.displayName} artifact in $artifactDir")
|
?: throw CommandInterrupted(text("sourceBuild.error.artifactNotFound", spec.displayName, artifactDir))
|
||||||
|
|
||||||
Files.createDirectories(targetPath.parent)
|
Files.createDirectories(targetPath.parent)
|
||||||
Files.copy(artifact, targetPath, StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(artifact, targetPath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
prompt.success("${spec.displayName} installed at $targetPath")
|
prompt.success(text("sourceBuild.success.installed", spec.displayName, targetPath))
|
||||||
} finally {
|
} finally {
|
||||||
runCatching { tempDir.toFile().deleteRecursively() }
|
runCatching { tempDir.toFile().deleteRecursively() }
|
||||||
}
|
}
|
||||||
@@ -83,31 +84,31 @@ fun buildAndInstallFromSource(
|
|||||||
private fun checkTool() {
|
private fun checkTool() {
|
||||||
fun buildToolLackedMessage(lack: Map<String, String>): String {
|
fun buildToolLackedMessage(lack: Map<String, String>): String {
|
||||||
return buildString {
|
return buildString {
|
||||||
appendLine("Missing required build tools:")
|
appendLine(text("sourceBuild.tool.missing.header"))
|
||||||
appendLine()
|
appendLine()
|
||||||
lack.forEach { (tool, reason) ->
|
lack.forEach { (tool, reason) ->
|
||||||
appendLine("$tool:")
|
appendLine(text("sourceBuild.tool.missing.item", tool))
|
||||||
appendLine(" $reason")
|
appendLine(text("sourceBuild.tool.missing.reason", reason))
|
||||||
}
|
}
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine("Install the missing tools, then rerun:")
|
appendLine(text("sourceBuild.tool.missing.footer"))
|
||||||
appendLine(" partnerctl init")
|
appendLine(" partnerctl init")
|
||||||
}.trimEnd()
|
}.trimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
val lack = mutableMapOf<String, String>()
|
val lack = mutableMapOf<String, String>()
|
||||||
if (runCommand(listOf("java", "--version")).exitCode != 0) {
|
if (runCommand(listOf("java", "--version")).exitCode != 0) {
|
||||||
lack["java"] = "Required to run Maven. Command failed: java --version"
|
lack["java"] = text("sourceBuild.tool.java.reason")
|
||||||
}
|
}
|
||||||
if (runCommand(listOf("javac", "--version")).exitCode != 0) {
|
if (runCommand(listOf("javac", "--version")).exitCode != 0) {
|
||||||
lack["javac"] =
|
lack["javac"] =
|
||||||
"Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version"
|
text("sourceBuild.tool.javac.reason")
|
||||||
}
|
}
|
||||||
if (runCommand(listOf("git", "--version")).exitCode != 0) {
|
if (runCommand(listOf("git", "--version")).exitCode != 0) {
|
||||||
lack["git"] = "Required to clone Partner source. Command failed: git --version"
|
lack["git"] = text("sourceBuild.tool.git.reason")
|
||||||
}
|
}
|
||||||
if (runCommand(listOf("mvn", "--version")).exitCode != 0) {
|
if (runCommand(listOf("mvn", "--version")).exitCode != 0) {
|
||||||
lack["mvn"] = "Required to build Partner from source. Command failed: mvn --version"
|
lack["mvn"] = text("sourceBuild.tool.mvn.reason")
|
||||||
}
|
}
|
||||||
if (lack.isNotEmpty()) {
|
if (lack.isNotEmpty()) {
|
||||||
throw CommandInterrupted(buildToolLackedMessage(lack))
|
throw CommandInterrupted(buildToolLackedMessage(lack))
|
||||||
|
|||||||
91
PartnerCtl/src/main/resources/i18n/messages.properties
Normal file
91
PartnerCtl/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
init.home.section=Initialize Partner Home
|
||||||
|
init.home.label=Partner Home
|
||||||
|
init.home.success=Partner Home initialized at {0}
|
||||||
|
init.home.duplicate.warning=Partner Home already contains files: {0}
|
||||||
|
init.home.duplicate.choice.label=Partner Home already contains files. Choose how to continue
|
||||||
|
init.home.duplicate.choice.another=Use another Partner Home
|
||||||
|
init.home.duplicate.choice.another.description=Choose a different directory
|
||||||
|
init.home.duplicate.choice.overwrite=Overwrite current Partner Home
|
||||||
|
init.home.duplicate.choice.overwrite.description=Delete existing contents and continue
|
||||||
|
init.home.duplicate.choice.exit=Cancel init
|
||||||
|
init.home.duplicate.confirmDelete=Delete all files under {0}?
|
||||||
|
init.home.notDirectory=Partner Home is not a directory: {0}
|
||||||
|
init.home.overwrite.refuseRoot=Refuse to overwrite filesystem root: {0}
|
||||||
|
init.home.overwrite.refuseUserHome=Refuse to overwrite user home directory: {0}
|
||||||
|
init.home.overwrite.refuseBroadDirectory=Refuse to overwrite suspiciously broad directory: {0}
|
||||||
|
init.install.section=Install Partner
|
||||||
|
init.install.method.label=Choose an installation method
|
||||||
|
init.install.method.buildFromSource=Build Partner from source
|
||||||
|
init.gateway.section=Configure Gateway
|
||||||
|
init.gateway.select.label=Select gateway
|
||||||
|
init.gateway.websocket.choice=WebSocket Gateway
|
||||||
|
init.gateway.warn.notFound=Could not find gateway with id {0}
|
||||||
|
init.gateway.warn.skipped=Gateway: {0} configuration skipped
|
||||||
|
init.gateway.info.skippedUseDefault=Skipped gateway configuration. Partner will use WebSocket as default gateway
|
||||||
|
init.gateway.defaultChannel.label=Set default channel
|
||||||
|
init.gateway.info.defaultChannel=The default channel will be set to {0}
|
||||||
|
init.gateway.success.configWritten=Gateway config written to {0}
|
||||||
|
init.model.section=Configure Model
|
||||||
|
init.model.provider.default.label=Choose default model provider type
|
||||||
|
init.model.provider.label=Choose model provider type
|
||||||
|
init.model.warn.noDefaultProvider=No default model provider configured. Partner may not start normally unless model.json exists or PARTNER_DEFAULT_BASE_URL, PARTNER_DEFAULT_API_KEY, and PARTNER_DEFAULT_MODEL are provided at runtime.
|
||||||
|
init.model.confirm.skipConfiguration=Skip model configuration?
|
||||||
|
init.model.confirm.addAdditionalProvider=Add additional model provider?
|
||||||
|
init.model.success.configWritten=Model config written to {0}
|
||||||
|
init.finish.section=Finish
|
||||||
|
init.finish.confirm.startNow=Start Partner now?
|
||||||
|
init.finish.info.completed=Initialization completed.
|
||||||
|
init.finish.info.starting=Starting Partner...
|
||||||
|
|
||||||
|
configure.gateway.websocket.section=Configure Gateway: WebSocket Gateway
|
||||||
|
configure.gateway.websocket.port.label=port
|
||||||
|
configure.gateway.websocket.port.error.int=WebSocket port only accepts int value
|
||||||
|
configure.gateway.websocket.port.error.range=WebSocket port should be between 1 and 65565
|
||||||
|
configure.gateway.websocket.heartbeatInterval.label=heartbeat interval
|
||||||
|
configure.gateway.websocket.heartbeatInterval.error.long=Heartbeat interval only accepts long value
|
||||||
|
configure.gateway.websocket.hostname.label=Listening hostname
|
||||||
|
configure.gateway.websocket.hostname.error.required=Hostname is required
|
||||||
|
configure.gateway.websocket.hostname.error.whitespace=Hostname must not contain whitespace.
|
||||||
|
configure.gateway.websocket.hostname.error.protocol=Do not include protocol. Use hostname only, for example: 127.0.0.1
|
||||||
|
configure.gateway.websocket.hostname.error.path=Do not include path. Use hostname only.
|
||||||
|
configure.gateway.websocket.hostname.error.port=Do not include port here. Port is configured separately.
|
||||||
|
configure.gateway.external.section=Configure Gateway: {0}
|
||||||
|
configure.gateway.external.details.title=Gateway module details
|
||||||
|
configure.gateway.external.details.description=Description
|
||||||
|
configure.gateway.external.details.source=Source
|
||||||
|
configure.gateway.external.details.buildCommand=Build command
|
||||||
|
configure.gateway.external.details.artifact=Artifact
|
||||||
|
configure.gateway.external.details.installTarget=Install target
|
||||||
|
configure.gateway.external.details.configTarget=Config target
|
||||||
|
configure.gateway.external.details.noConfig=No config
|
||||||
|
configure.gateway.external.confirmContinue=Continue installation?
|
||||||
|
configure.field.error.int={0} only accepts int value
|
||||||
|
configure.field.error.number={0} only accepts number value
|
||||||
|
configure.field.error.boolean={0} only accepts true or false
|
||||||
|
configure.field.error.rawJson={0} only accepts valid JSON
|
||||||
|
configure.model.openAiCompatible.providerName.label=Provider name
|
||||||
|
configure.model.openAiCompatible.providerName.error.duplicateDefault=Default provider cannot be duplicate
|
||||||
|
configure.model.openAiCompatible.baseUrl.label=Base url
|
||||||
|
configure.model.openAiCompatible.apiKey.label=Apikey
|
||||||
|
configure.model.openAiCompatible.defaultModel.label=Default model
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.required=Base url is required
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.validUrl=Base url must be a valid URL
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.scheme=Base url must start with http:// or https://
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.host=Base url must include a valid host
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.userInfo=Base url must not include user info
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.fragment=Base url must not include fragment
|
||||||
|
|
||||||
|
sourceBuild.info.cloning=Cloning {0} source from {1}
|
||||||
|
sourceBuild.error.cloneFailed=Failed to clone {0} source from {1}
|
||||||
|
sourceBuild.info.building=Building {0}
|
||||||
|
sourceBuild.error.buildFailed=Failed to build {0}.
|
||||||
|
sourceBuild.error.artifactNotFound=Could not find built {0} artifact in {1}
|
||||||
|
sourceBuild.success.installed={0} installed at {1}
|
||||||
|
sourceBuild.tool.missing.header=Missing required build tools:
|
||||||
|
sourceBuild.tool.missing.item={0}:
|
||||||
|
sourceBuild.tool.missing.reason= {0}
|
||||||
|
sourceBuild.tool.missing.footer=Install the missing tools, then rerun:
|
||||||
|
sourceBuild.tool.java.reason=Required to run Maven. Command failed: java --version
|
||||||
|
sourceBuild.tool.javac.reason=Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version
|
||||||
|
sourceBuild.tool.git.reason=Required to clone Partner source. Command failed: git --version
|
||||||
|
sourceBuild.tool.mvn.reason=Required to build Partner from source. Command failed: mvn --version
|
||||||
91
PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties
Normal file
91
PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
init.home.section=初始化 Partner Home
|
||||||
|
init.home.label=Partner Home
|
||||||
|
init.home.success=Partner Home 已初始化:{0}
|
||||||
|
init.home.duplicate.warning=Partner Home 已包含文件:{0}
|
||||||
|
init.home.duplicate.choice.label=Partner Home 已包含文件,请选择如何继续
|
||||||
|
init.home.duplicate.choice.another=使用另一个 Partner Home
|
||||||
|
init.home.duplicate.choice.another.description=重新选择一个目录
|
||||||
|
init.home.duplicate.choice.overwrite=覆盖当前 Partner Home
|
||||||
|
init.home.duplicate.choice.overwrite.description=删除现有内容并继续
|
||||||
|
init.home.duplicate.choice.exit=取消初始化
|
||||||
|
init.home.duplicate.confirmDelete=确定删除 {0} 下的所有文件?
|
||||||
|
init.home.notDirectory=Partner Home 不是目录:{0}
|
||||||
|
init.home.overwrite.refuseRoot=拒绝覆盖文件系统根目录:{0}
|
||||||
|
init.home.overwrite.refuseUserHome=拒绝覆盖用户主目录:{0}
|
||||||
|
init.home.overwrite.refuseBroadDirectory=拒绝覆盖范围过大的目录:{0}
|
||||||
|
init.install.section=安装 Partner
|
||||||
|
init.install.method.label=选择安装方式
|
||||||
|
init.install.method.buildFromSource=从源码构建 Partner
|
||||||
|
init.gateway.section=配置网关
|
||||||
|
init.gateway.select.label=选择网关
|
||||||
|
init.gateway.websocket.choice=WebSocket Gateway
|
||||||
|
init.gateway.warn.notFound=找不到 id 为 {0} 的网关
|
||||||
|
init.gateway.warn.skipped=网关:{0} 配置已跳过
|
||||||
|
init.gateway.info.skippedUseDefault=已跳过网关配置。Partner 将使用 WebSocket 作为默认网关
|
||||||
|
init.gateway.defaultChannel.label=设置默认 channel
|
||||||
|
init.gateway.info.defaultChannel=默认 channel 将设置为 {0}
|
||||||
|
init.gateway.success.configWritten=网关配置已写入 {0}
|
||||||
|
init.model.section=配置模型
|
||||||
|
init.model.provider.default.label=选择默认模型 provider 类型
|
||||||
|
init.model.provider.label=选择模型 provider 类型
|
||||||
|
init.model.warn.noDefaultProvider=没有配置默认模型 provider。除非 model.json 已存在,或运行时提供 PARTNER_DEFAULT_BASE_URL、PARTNER_DEFAULT_API_KEY 和 PARTNER_DEFAULT_MODEL,否则 Partner 可能无法正常启动。
|
||||||
|
init.model.confirm.skipConfiguration=跳过模型配置?
|
||||||
|
init.model.confirm.addAdditionalProvider=添加额外模型 provider?
|
||||||
|
init.model.success.configWritten=模型配置已写入 {0}
|
||||||
|
init.finish.section=完成
|
||||||
|
init.finish.confirm.startNow=现在启动 Partner?
|
||||||
|
init.finish.info.completed=初始化完成。
|
||||||
|
init.finish.info.starting=正在启动 Partner...
|
||||||
|
|
||||||
|
configure.gateway.websocket.section=配置网关:WebSocket Gateway
|
||||||
|
configure.gateway.websocket.port.label=端口
|
||||||
|
configure.gateway.websocket.port.error.int=WebSocket 端口只接受整数
|
||||||
|
configure.gateway.websocket.port.error.range=WebSocket 端口应在 1 到 65565 之间
|
||||||
|
configure.gateway.websocket.heartbeatInterval.label=心跳间隔
|
||||||
|
configure.gateway.websocket.heartbeatInterval.error.long=心跳间隔只接受 long 数值
|
||||||
|
configure.gateway.websocket.hostname.label=监听主机名
|
||||||
|
configure.gateway.websocket.hostname.error.required=主机名不能为空
|
||||||
|
configure.gateway.websocket.hostname.error.whitespace=主机名不能包含空白字符。
|
||||||
|
configure.gateway.websocket.hostname.error.protocol=不要包含协议。只填写主机名,例如:127.0.0.1
|
||||||
|
configure.gateway.websocket.hostname.error.path=不要包含路径。只填写主机名。
|
||||||
|
configure.gateway.websocket.hostname.error.port=不要在这里填写端口。端口会单独配置。
|
||||||
|
configure.gateway.external.section=配置网关:{0}
|
||||||
|
configure.gateway.external.details.title=网关模块详情
|
||||||
|
configure.gateway.external.details.description=描述
|
||||||
|
configure.gateway.external.details.source=来源
|
||||||
|
configure.gateway.external.details.buildCommand=构建命令
|
||||||
|
configure.gateway.external.details.artifact=构建产物
|
||||||
|
configure.gateway.external.details.installTarget=安装目标
|
||||||
|
configure.gateway.external.details.configTarget=配置目标
|
||||||
|
configure.gateway.external.details.noConfig=无配置
|
||||||
|
configure.gateway.external.confirmContinue=继续安装?
|
||||||
|
configure.field.error.int={0} 只接受整数
|
||||||
|
configure.field.error.number={0} 只接受数字
|
||||||
|
configure.field.error.boolean={0} 只接受 true 或 false
|
||||||
|
configure.field.error.rawJson={0} 只接受合法 JSON
|
||||||
|
configure.model.openAiCompatible.providerName.label=Provider 名称
|
||||||
|
configure.model.openAiCompatible.providerName.error.duplicateDefault=Default provider 不能重复
|
||||||
|
configure.model.openAiCompatible.baseUrl.label=Base url
|
||||||
|
configure.model.openAiCompatible.apiKey.label=Apikey
|
||||||
|
configure.model.openAiCompatible.defaultModel.label=Default model
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.required=Base url 不能为空
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.validUrl=Base url 必须是合法 URL
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.scheme=Base url 必须以 http:// 或 https:// 开头
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.host=Base url 必须包含合法 host
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.userInfo=Base url 不能包含 user info
|
||||||
|
configure.model.openAiCompatible.baseUrl.error.fragment=Base url 不能包含 fragment
|
||||||
|
|
||||||
|
sourceBuild.info.cloning=正在从 {1} 克隆 {0} 源码
|
||||||
|
sourceBuild.error.cloneFailed=从 {1} 克隆 {0} 源码失败
|
||||||
|
sourceBuild.info.building=正在构建 {0}
|
||||||
|
sourceBuild.error.buildFailed=构建 {0} 失败。
|
||||||
|
sourceBuild.error.artifactNotFound=在 {1} 中找不到已构建的 {0} 产物
|
||||||
|
sourceBuild.success.installed={0} 已安装到 {1}
|
||||||
|
sourceBuild.tool.missing.header=缺少必要的构建工具:
|
||||||
|
sourceBuild.tool.missing.item={0}:
|
||||||
|
sourceBuild.tool.missing.reason= {0}
|
||||||
|
sourceBuild.tool.missing.footer=安装缺失工具后重新运行:
|
||||||
|
sourceBuild.tool.java.reason=运行 Maven 需要 java。命令失败:java --version
|
||||||
|
sourceBuild.tool.javac.reason=从源码编译 Partner 需要 javac。请安装 JDK,而不只是 JRE。命令失败:javac --version
|
||||||
|
sourceBuild.tool.git.reason=克隆 Partner 源码需要 git。命令失败:git --version
|
||||||
|
sourceBuild.tool.mvn.reason=从源码构建 Partner 需要 mvn。命令失败:mvn --version
|
||||||
Reference in New Issue
Block a user