feat(partnerctl): add --log-level option for run command and wire runtime log level to Partner-Core

This commit is contained in:
2026-05-09 14:12:32 +08:00
parent d72b34acfd
commit 8cb876b532
6 changed files with 154 additions and 115 deletions

37
.idea/misc.xml generated
View File

@@ -1,27 +1,28 @@
<?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="20"> <list size="21">
<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="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" /> <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.CapabilityCore" /> <item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" /> <item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" /> <item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" /> <item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" /> <item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" /> <item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" /> <item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" /> <item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" /> <item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" /> <item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" /> <item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" /> <item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" /> <item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" /> <item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" /> <item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
<item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" /> <item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
<item index="20" 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" />

View File

@@ -1,6 +1,7 @@
<configuration> <configuration>
<property name="PARTNER_HOME" value="${PARTNER_HOME:-${user.home}/.partner}"/> <property name="PARTNER_HOME" value="${PARTNER_HOME:-${user.home}/.partner}"/>
<property name="PARTNER_LOG_DIR" value="${PARTNER_HOME}/state/trace/log"/> <property name="PARTNER_LOG_DIR" value="${PARTNER_HOME}/state/trace/log"/>
<property name="LOG_LEVEL" value="${PARTNER_LOG_LEVEL}:-${partner.log.level:-INFO}"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
@@ -24,7 +25,7 @@
</encoder> </encoder>
</appender> </appender>
<root level="DEBUG"> <root level="${LOG_LEVEL}">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
<appender-ref ref="PARTNER_FILE"/> <appender-ref ref="PARTNER_FILE"/>
</root> </root>

View File

@@ -4,11 +4,7 @@ import picocli.CommandLine
import work.slhaf.partner.ctl.commands.control.* import work.slhaf.partner.ctl.commands.control.*
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 java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.time.LocalDateTime
@CommandLine.Command( @CommandLine.Command(
name = "run", name = "run",
@@ -26,6 +22,14 @@ class RunCommand : Runnable {
) )
var background: Boolean = false var background: Boolean = false
@CommandLine.Option(
names = ["-l", "--log-level"],
descriptionKey = "cli.log.option.level.description",
defaultValue = "INFO",
converter = [LogLevelConverter::class],
)
lateinit var logLevel: LogLevel
override fun run() { override fun run() {
val home = resolvePartnerHome() val home = resolvePartnerHome()
val partnerJar = resolvePartnerJar(home) val partnerJar = resolvePartnerJar(home)
@@ -35,98 +39,10 @@ class RunCommand : Runnable {
} }
if (background) { if (background) {
runInBackground(home, partnerJar) runInBackground(home, partnerJar, logLevel)
} else { } else {
runInForeground(home, partnerJar) runInForeground(home, partnerJar, logLevel)
} }
} }
} }
private fun runInForeground(home: Path, partnerJar: Path) {
val logFile = resolveLogFile(home)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar)
.inheritIO()
.start()
appendControlLog(logFile, text("control.run.log.foregroundStarting", process.pid(), partnerJar))
val shutdownHook = Thread {
if (process.isAlive) {
process.destroy()
}
}
Runtime.getRuntime().addShutdownHook(shutdownHook)
val exitCode = process.waitFor()
runCatching { Runtime.getRuntime().removeShutdownHook(shutdownHook) }
appendControlLog(logFile, text("control.run.log.exited", exitCode))
if (exitCode != 0) {
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
}
private fun runInBackground(home: Path, partnerJar: Path) {
val pidFile = resolvePidFile(home)
val logFile = resolveLogFile(home)
val existingProcess = findPartnerProcesses(home, partnerJar).firstOrNull()
if (existingProcess != null) {
throw CommandInterrupted(text("control.run.error.alreadyRunning", existingProcess.pid()))
}
Files.createDirectories(pidFile.parent)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start()
appendControlLog(logFile, text("control.run.log.backgroundStarting", process.pid(), partnerJar))
Thread.sleep(BACKGROUND_START_CHECK_MILLIS)
if (!process.isAlive) {
val exitCode = process.exitValue()
appendControlLog(logFile, text("control.run.log.exited", exitCode))
Files.deleteIfExists(pidFile)
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
Files.writeString(
pidFile,
process.pid().toString(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
)
println(text("control.run.success.backgroundStarted", process.pid()))
println(text("control.run.info.logFile", logFile))
}
private fun createPartnerProcessBuilder(home: Path, partnerJar: Path): ProcessBuilder {
return ProcessBuilder("java", "-DPARTNER_HOME=$home", "-jar", partnerJar.toString())
.apply {
environment()["PARTNER_HOME"] = home.toString()
}
}
private fun appendControlLog(logFile: Path, message: String) {
Files.createDirectories(logFile.parent)
Files.newOutputStream(
logFile,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
).use { output ->
writeControlLog(output, message)
}
}
private fun writeControlLog(output: OutputStream, message: String) {
output.write("[partnerctl ${LocalDateTime.now()}] $message\n".toByteArray())
output.flush()
}
private const val BACKGROUND_START_CHECK_MILLIS = 500L

View File

@@ -1,8 +1,14 @@
package work.slhaf.partner.ctl.commands.control package work.slhaf.partner.ctl.commands.control
import picocli.CommandLine
import work.slhaf.partner.ctl.i18n.I18n.text
import work.slhaf.partner.ctl.support.CommandInterrupted
import java.io.OutputStream
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 java.nio.file.StandardOpenOption
import java.time.LocalDateTime
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
@@ -25,7 +31,8 @@ fun resolvePidFile(home: Path): Path {
} }
fun resolveLogFile(home: Path): Path { fun resolveLogFile(home: Path): Path {
return home.resolve("state").resolve("trace").resolve("log").resolve("partner-core.log").toAbsolutePath().normalize() return home.resolve("state").resolve("trace").resolve("log").resolve("partner-core.log").toAbsolutePath()
.normalize()
} }
fun findPartnerProcesses(home: Path, partnerJar: Path): List<ProcessHandle> { fun findPartnerProcesses(home: Path, partnerJar: Path): List<ProcessHandle> {
@@ -95,3 +102,115 @@ private fun isPartnerProcess(process: ProcessHandle, partnerJar: Path): Boolean
return arguments.any { it == jarPath } || commandLine.contains(jarPath) return arguments.any { it == jarPath } || commandLine.contains(jarPath)
} }
fun runInForeground(home: Path, partnerJar: Path, logLevel: LogLevel) {
val logFile = resolveLogFile(home)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar, logLevel)
.inheritIO()
.start()
appendControlLog(logFile, text("control.run.log.foregroundStarting", process.pid(), partnerJar))
val shutdownHook = Thread {
if (process.isAlive) {
process.destroy()
}
}
Runtime.getRuntime().addShutdownHook(shutdownHook)
val exitCode = process.waitFor()
runCatching { Runtime.getRuntime().removeShutdownHook(shutdownHook) }
appendControlLog(logFile, text("control.run.log.exited", exitCode))
if (exitCode != 0) {
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
}
fun runInBackground(home: Path, partnerJar: Path, logLevel: LogLevel) {
val pidFile = resolvePidFile(home)
val logFile = resolveLogFile(home)
val existingProcess = findPartnerProcesses(home, partnerJar).firstOrNull()
if (existingProcess != null) {
throw CommandInterrupted(text("control.run.error.alreadyRunning", existingProcess.pid()))
}
Files.createDirectories(pidFile.parent)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar, logLevel)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start()
appendControlLog(logFile, text("control.run.log.backgroundStarting", process.pid(), partnerJar))
Thread.sleep(BACKGROUND_START_CHECK_MILLIS)
if (!process.isAlive) {
val exitCode = process.exitValue()
appendControlLog(logFile, text("control.run.log.exited", exitCode))
Files.deleteIfExists(pidFile)
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
Files.writeString(
pidFile,
process.pid().toString(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
)
println(text("control.run.success.backgroundStarted", process.pid()))
println(text("control.run.info.logFile", logFile))
}
private fun createPartnerProcessBuilder(home: Path, partnerJar: Path, logLevel: LogLevel): ProcessBuilder {
return ProcessBuilder(
"java",
"-Dpartner.log.level=${logLevel.name.uppercase()}",
"-DPARTNER_HOME=$home",
"-jar",
partnerJar.toString()
)
.apply {
environment()["PARTNER_HOME"] = home.toString()
}
}
private fun appendControlLog(logFile: Path, message: String) {
Files.createDirectories(logFile.parent)
Files.newOutputStream(
logFile,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
).use { output ->
writeControlLog(output, message)
}
}
private fun writeControlLog(output: OutputStream, message: String) {
output.write("[partnerctl ${LocalDateTime.now()}] $message\n".toByteArray())
output.flush()
}
private const val BACKGROUND_START_CHECK_MILLIS = 500L
enum class LogLevel {
TRACE, DEBUG, INFO, WARN, ERROR
}
class LogLevelConverter : CommandLine.ITypeConverter<LogLevel> {
override fun convert(value: String): LogLevel {
return LogLevel.entries.firstOrNull {
it.name.equals(value, ignoreCase = true)
} ?: throw CommandLine.TypeConversionException(
"invalid log level '$value'. Valid values: ${
LogLevel.entries.joinToString(", ") { it.name.lowercase() }
}"
)
}
}

View File

@@ -8,6 +8,7 @@ cli.shutdown.option.force.description=Forcefully kill matching Partner process i
cli.log.description=Show Partner logs. cli.log.description=Show Partner logs.
cli.log.option.tail.description=Number of log lines to show before exiting or following. cli.log.option.tail.description=Number of log lines to show before exiting or following.
cli.log.option.follow.description=Follow appended log output. cli.log.option.follow.description=Follow appended log output.
cli.log.option.level.description=Set Partner runtime log level. Available values: TRACE, DEBUG, INFO, WARN, ERROR. Default: INFO.
cli.chat.description=Start an interactive chat client. cli.chat.description=Start an interactive chat client.
cli.config.description=Manage Partner configuration. cli.config.description=Manage Partner configuration.
cli.module.description=Manage Partner modules. cli.module.description=Manage Partner modules.

View File

@@ -2,12 +2,13 @@ cli.partnerctl.description=Partner 命令行工具。
cli.init.description=初始化 Partner agent。 cli.init.description=初始化 Partner agent。
cli.run.description=启动 Partner agent。 cli.run.description=启动 Partner agent。
cli.run.option.background.description=后台运行 Partner。 cli.run.option.background.description=后台运行 Partner。
cli.shutdown.description=停止 Partner agent。
cli.shutdown.option.timeout.description=优雅停止后等待的秒数,超时后失败或强制停止。 cli.shutdown.option.timeout.description=优雅停止后等待的秒数,超时后失败或强制停止。
cli.shutdown.option.force.description=如果匹配的 Partner 进程没有在超时前退出,则强制结束进程。 cli.shutdown.option.force.description=如果匹配的 Partner 进程没有在超时前退出,则强制结束进程。
cli.log.description=查看 Partner 日志。 cli.log.description=查看 Partner 日志。
cli.log.option.tail.description=退出或 follow 前显示的日志行数。 cli.log.option.tail.description=退出或 follow 前显示的日志行数。
cli.log.option.follow.description=持续跟随新增日志输出。 cli.log.option.follow.description=持续跟随新增日志输出。
cli.log.option.level.description=指定 Partner 运行时日志等级。可选值TRACE、DEBUG、INFO、WARN、ERROR。默认值INFO。
cli.shutdown.description=停止 Partner agent。
cli.chat.description=启动交互式聊天客户端。 cli.chat.description=启动交互式聊天客户端。
cli.config.description=管理 Partner 配置。 cli.config.description=管理 Partner 配置。
cli.module.description=管理 Partner 模块。 cli.module.description=管理 Partner 模块。