Skip to content

Commit 94692a3

Browse files
committed
feat: built-in plugins
close #10
1 parent bc48f71 commit 94692a3

File tree

10 files changed

+260
-62
lines changed

10 files changed

+260
-62
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package org.ntqqrev.saltify.builtin.plugin
2+
3+
import org.ntqqrev.saltify.core.forward
4+
import org.ntqqrev.saltify.core.node
5+
import org.ntqqrev.saltify.core.text
6+
import org.ntqqrev.saltify.dsl.SaltifyPlugin
7+
import org.ntqqrev.saltify.entity.RegisteredCommandInfo
8+
import org.ntqqrev.saltify.entity.RegisteredSubCommandInfo
9+
import org.ntqqrev.saltify.extension.respond
10+
11+
/**
12+
* Saltify 内置全局帮助指令插件。
13+
*/
14+
public val commandHelp: SaltifyPlugin<Unit> = SaltifyPlugin("command-help") {
15+
command("help") {
16+
description = "显示所有已注册指令的帮助信息"
17+
18+
onExecute {
19+
val registry = client.commandRegistry
20+
if (registry.isEmpty()) {
21+
respond("当前没有已注册的指令。")
22+
return@onExecute
23+
}
24+
25+
val grouped = registry
26+
.groupBy { it.pluginName }
27+
.entries
28+
.sortedWith(compareBy { it.key ?: "\uFFFF" })
29+
30+
respond {
31+
forward {
32+
node(event.senderId, "Command Help") {
33+
text(buildString {
34+
appendLine("指令帮助")
35+
append("当前共注册了 ${registry.size} 条指令")
36+
})
37+
}
38+
39+
for ((pluginName, commands) in grouped) {
40+
node(event.senderId, "Command Help") {
41+
text(buildCommandGroupText(pluginName, commands))
42+
}
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}
49+
50+
private fun buildCommandGroupText(
51+
pluginName: String?,
52+
commands: List<RegisteredCommandInfo>
53+
): String = buildString {
54+
if (pluginName != null) {
55+
appendLine("插件 $pluginName: ")
56+
} else {
57+
appendLine("其他指令: ")
58+
}
59+
60+
for (cmd in commands) {
61+
appendLine()
62+
// 指令名 + 描述
63+
val header = "${cmd.prefix}${cmd.name}"
64+
if (cmd.description.isNotEmpty()) {
65+
appendLine("$header - ${cmd.description}")
66+
} else {
67+
appendLine(header)
68+
}
69+
// 参数
70+
if (cmd.parameters.isNotEmpty()) {
71+
val params = cmd.parameters.joinToString(" ") { p ->
72+
"<${p.name}: ${p.type.simpleName ?: p.type}>"
73+
}
74+
appendLine(" 参数: $params")
75+
}
76+
// 子指令
77+
for (sub in cmd.subCommands) {
78+
appendSubCommand(sub, "${cmd.prefix}${cmd.name}", depth = 1)
79+
}
80+
}
81+
}.trimEnd()
82+
83+
private fun StringBuilder.appendSubCommand(
84+
sub: RegisteredSubCommandInfo,
85+
parentPath: String,
86+
depth: Int
87+
) {
88+
val indent = " ".repeat(depth)
89+
val path = "$parentPath ${sub.name}"
90+
if (sub.description.isNotEmpty()) {
91+
appendLine("$indent$path - ${sub.description}")
92+
} else {
93+
appendLine("$indent$path")
94+
}
95+
if (sub.parameters.isNotEmpty()) {
96+
val params = sub.parameters.joinToString(" ") { p ->
97+
"<${p.name}: ${p.type.simpleName ?: p.type}>"
98+
}
99+
appendLine("$indent 参数: $params")
100+
}
101+
for (nested in sub.subCommands) {
102+
appendSubCommand(nested, path, depth + 1)
103+
}
104+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.ntqqrev.saltify.builtin.plugin
2+
3+
import kotlinx.coroutines.CancellationException
4+
import kotlinx.coroutines.launch
5+
import org.ntqqrev.milky.Event
6+
import org.ntqqrev.milky.IncomingMessage
7+
import org.ntqqrev.saltify.dsl.SaltifyPlugin
8+
import org.ntqqrev.saltify.extension.plainText
9+
import org.ntqqrev.saltify.model.EventConnectionState
10+
import org.ntqqrev.saltify.model.SaltifyComponentType
11+
import org.ntqqrev.saltify.util.coroutine.saltifyComponent
12+
13+
/**
14+
* Saltify 内置日志插件。
15+
*
16+
* 负责输出事件服务连接状态变更、未捕获异常以及收到的消息日志。
17+
*/
18+
public val defaultLogging: SaltifyPlugin<Unit> = SaltifyPlugin("default-logging") {
19+
// 事件服务连接状态日志
20+
launch {
21+
client.eventConnectionStateFlow.collect { state ->
22+
when (state) {
23+
is EventConnectionState.Connected ->
24+
client.logger.info("事件服务已连接, 使用协议:${state.type.name}")
25+
is EventConnectionState.Disconnected -> {
26+
val error = state.throwable
27+
if (error != null && error !is CancellationException)
28+
client.logger.error("事件服务已断开", error)
29+
}
30+
is EventConnectionState.Connecting ->
31+
client.logger.info("事件服务正在连接...")
32+
is EventConnectionState.Reconnecting ->
33+
client.logger.warn(
34+
"事件服务断开, 将在 ${state.delay}ms 后尝试重连... (重试次数: ${state.attempt})",
35+
state.throwable
36+
)
37+
}
38+
}
39+
}
40+
41+
// 未捕获异常日志
42+
launch {
43+
client.exceptionFlow.collect { (context, throwable) ->
44+
val component = context.saltifyComponent!!
45+
when (component.type) {
46+
SaltifyComponentType.Application ->
47+
client.logger.error("Saltify 根组件异常", throwable)
48+
SaltifyComponentType.Plugin ->
49+
client.logger.error("Saltify 插件 ${component.name} 异常", throwable)
50+
SaltifyComponentType.Extension ->
51+
client.logger.error("Saltify 基础扩展组件异常", throwable)
52+
}
53+
}
54+
}
55+
56+
// 收到消息日志
57+
on<Event.MessageReceive> { event ->
58+
when (val data = event.data) {
59+
is IncomingMessage.Group ->
60+
logger.debug(
61+
"${data.groupMember.userId}(${data.group.groupId}): ${event.segments.plainText}"
62+
)
63+
else ->
64+
logger.debug("${event.peerId}: ${event.segments.plainText}")
65+
}
66+
}
67+
}

saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/core/SaltifyApplication.kt

Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ import org.ntqqrev.saltify.annotation.WithApiExtension
1919
import org.ntqqrev.saltify.dsl.SaltifyPluginContext
2020
import org.ntqqrev.saltify.dsl.config.SaltifyApplicationConfig
2121
import org.ntqqrev.saltify.entity.InstalledPlugin
22+
import org.ntqqrev.saltify.entity.RegisteredCommandInfo
2223
import org.ntqqrev.saltify.exception.ApiCallException
23-
import org.ntqqrev.saltify.extension.plainText
2424
import org.ntqqrev.saltify.model.EventConnectionState
2525
import org.ntqqrev.saltify.model.EventConnectionType
2626
import org.ntqqrev.saltify.model.SaltifyComponentType
2727
import org.ntqqrev.saltify.util.coroutine.SaltifyComponent
2828
import org.ntqqrev.saltify.util.coroutine.SaltifyExceptionHandlerProvider
29-
import org.ntqqrev.saltify.util.coroutine.saltifyComponent
3029
import kotlin.coroutines.CoroutineContext
3130
import kotlin.time.Clock
3231

@@ -101,6 +100,8 @@ public sealed class SaltifyApplication(internal val config: SaltifyApplicationCo
101100

102101
private val loadedPlugins = mutableListOf<SaltifyPluginContext>()
103102

103+
internal val commandRegistry: MutableList<RegisteredCommandInfo> = mutableListOf()
104+
104105
@PublishedApi
105106
internal val httpClient: HttpClient = HttpClient {
106107
install(ContentNegotiation) {
@@ -125,14 +126,10 @@ public sealed class SaltifyApplication(internal val config: SaltifyApplicationCo
125126
}
126127
}
127128

128-
private lateinit var loggingListenerJob: Job
129-
130129
public suspend fun start(): SaltifyApplication {
131130
logger.info("Saltify 正在启动...")
132131
val startInstant = Clock.System.now()
133132

134-
loggingListenerJob = startLoggingListeners()
135-
136133
// 插件初始化
137134
config.installedPlugins.map { installed ->
138135
@Suppress("UNCHECKED_CAST")
@@ -198,64 +195,9 @@ public sealed class SaltifyApplication(internal val config: SaltifyApplicationCo
198195
loadedPlugins.forEach {
199196
it.onStopHooks.forEach { block -> block() }
200197
}
201-
loggingListenerJob.cancel()
202198
httpClient.close()
203199
applicationScope.cancel()
204200

205201
logger.info("Saltify 已关闭")
206202
}
207203
}
208-
209-
private fun SaltifyApplication.startLoggingListeners() = applicationScope.launch {
210-
// 事件服务状态日志
211-
applicationScope.launch {
212-
eventConnectionStateFlow.collect {
213-
when (it) {
214-
is EventConnectionState.Connected ->
215-
logger.info("事件服务已连接, 使用协议:${it.type.name}")
216-
is EventConnectionState.Disconnected -> {
217-
val error = it.throwable
218-
if (error != null && error !is CancellationException) logger.error("事件服务已断开", error)
219-
}
220-
is EventConnectionState.Connecting ->
221-
logger.info("事件服务正在连接...")
222-
is EventConnectionState.Reconnecting ->
223-
logger.warn("事件服务断开, 将在 ${it.delay}ms 后尝试重连... (重试次数: ${it.attempt})", it.throwable)
224-
}
225-
}
226-
}
227-
228-
// 错误日志
229-
applicationScope.launch {
230-
exceptionFlow.collect {
231-
val component = it.first.saltifyComponent!!
232-
when (component.type) {
233-
SaltifyComponentType.Application ->
234-
logger.error("Saltify 根组件异常", it.second)
235-
SaltifyComponentType.Plugin ->
236-
logger.error("Saltify 插件 ${component.name} 异常", it.second)
237-
SaltifyComponentType.Extension ->
238-
logger.error("Saltify 基础扩展组件异常", it.second)
239-
}
240-
}
241-
}
242-
243-
// 事件日志
244-
applicationScope.launch {
245-
eventFlow.collect {
246-
when (it) {
247-
is Event.MessageReceive -> {
248-
when (val data = it.data) {
249-
is IncomingMessage.Group ->
250-
logger.debug(
251-
"${data.groupMember.userId}(${data.group.groupId}): ${it.segments.plainText}"
252-
)
253-
else ->
254-
logger.debug("${it.peerId}: ${it.segments.plainText}")
255-
}
256-
}
257-
else -> {}
258-
}
259-
}
260-
}
261-
}

saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyCommandContext.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public class SaltifyCommandContext internal constructor() {
2727
internal val subCommands = mutableListOf<Pair<String, SaltifyCommandContext>>()
2828
internal val parameters = mutableListOf<SaltifyCommandParamDef<*>>()
2929
internal var executionBlock: (suspend SaltifyCommandExecutionContext.() -> Unit)? = null
30+
31+
/**
32+
* 指令的描述信息。
33+
*/
34+
public var description: String = ""
3035
internal var groupExecutionBlock: (suspend SaltifyCommandExecutionContext.() -> Unit)? = null
3136
internal var privateExecutionBlock: (suspend SaltifyCommandExecutionContext.() -> Unit)? = null
3237
internal var failureBlock: (suspend SaltifyCommandExecutionContext.(CommandError) -> Unit)? = null
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.ntqqrev.saltify.entity
2+
3+
import org.ntqqrev.saltify.dsl.SaltifyCommandParamDef
4+
5+
/**
6+
* 子指令的注册信息
7+
*/
8+
public data class RegisteredSubCommandInfo(
9+
val name: String,
10+
val description: String,
11+
val parameters: List<SaltifyCommandParamDef<*>>,
12+
val subCommands: List<RegisteredSubCommandInfo> = emptyList()
13+
)
14+
15+
/**
16+
* 已注册指令的信息
17+
*/
18+
public data class RegisteredCommandInfo(
19+
val name: String,
20+
val prefix: String,
21+
val description: String,
22+
val parameters: List<SaltifyCommandParamDef<*>>,
23+
val subCommands: List<RegisteredSubCommandInfo>,
24+
val pluginName: String?
25+
)

saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/ApplicationExtension.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import org.ntqqrev.saltify.dsl.ParameterParseResult
1212
import org.ntqqrev.saltify.dsl.SaltifyCommandContext
1313
import org.ntqqrev.saltify.dsl.SaltifyCommandExecutionContext
1414
import org.ntqqrev.saltify.dsl.SaltifyCommandParamDef
15+
import org.ntqqrev.saltify.entity.RegisteredCommandInfo
16+
import org.ntqqrev.saltify.entity.RegisteredSubCommandInfo
1517
import org.ntqqrev.saltify.entity.SaltifyBotConfig
1618
import org.ntqqrev.saltify.entity.SaltifyCommandRequirementContext
1719
import org.ntqqrev.saltify.model.CommandError
20+
import org.ntqqrev.saltify.model.SaltifyComponentType
1821
import org.ntqqrev.saltify.util.coroutine.runCatchingToExceptionFlow
22+
import org.ntqqrev.saltify.util.coroutine.saltifyComponent
1923
import kotlin.reflect.KClass
2024
import kotlin.time.Clock
2125

@@ -67,6 +71,21 @@ public fun SaltifyApplication.command(
6771
): Job {
6872
val rootDsl = SaltifyCommandContext().apply(builder)
6973

74+
val component = scope.coroutineContext.saltifyComponent
75+
val pluginName = if (component?.type == SaltifyComponentType.Plugin) component.name else null
76+
commandRegistry.add(
77+
RegisteredCommandInfo(
78+
name = name,
79+
prefix = prefix,
80+
description = rootDsl.description,
81+
parameters = rootDsl.parameters.toList(),
82+
subCommands = rootDsl.subCommands.map { (subName, subCtx) ->
83+
subCtx.toSubCommandInfo(subName)
84+
},
85+
pluginName = pluginName
86+
)
87+
)
88+
7089
return on<Event.MessageReceive>(scope) { event ->
7190
val rawText = event.segments.filterIsInstance<IncomingSegment.Text>()
7291
.joinToString("") { it.text }
@@ -144,6 +163,14 @@ private suspend fun executeCommand(
144163
execution.logger.info("seq=${event.messageSeq} 处理完成, 用时 ${Clock.System.now() - startInstant}")
145164
}
146165

166+
private fun SaltifyCommandContext.toSubCommandInfo(name: String): RegisteredSubCommandInfo =
167+
RegisteredSubCommandInfo(
168+
name = name,
169+
description = description,
170+
parameters = parameters.toList(),
171+
subCommands = subCommands.map { (subName, subCtx) -> subCtx.toSubCommandInfo(subName) }
172+
)
173+
147174
@Suppress("UNCHECKED_CAST")
148175
private fun <T : Any> convertValue(value: String, type: KClass<T>): T? {
149176
return when (type) {

saltify-core/src/jvmTest/kotlin/org/ntqqrev/saltify/PluginTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package org.ntqqrev.saltify
33
import kotlinx.coroutines.delay
44
import kotlinx.coroutines.runBlocking
55
import org.ntqqrev.milky.IncomingMessage
6+
import org.ntqqrev.saltify.builtin.plugin.commandHelp
7+
import org.ntqqrev.saltify.builtin.plugin.defaultLogging
68
import org.ntqqrev.saltify.core.SaltifyApplication
79
import org.ntqqrev.saltify.core.getLoginInfo
810
import org.ntqqrev.saltify.dsl.SaltifyPlugin
@@ -32,6 +34,9 @@ class PluginTest {
3234
install(testPlugin) {
3335
response = "Hello!!"
3436
}
37+
38+
install(commandHelp)
39+
install(defaultLogging)
3540
}.start()
3641

3742
client.connectEvent()

saltify-docs/content/docs-core/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export default {
66
command: '指令开发',
77
permission: '权限管理',
88
logging: '日志实现',
9+
'builtin-plugins': '内置插件',
910
} satisfies MetaRecord;

0 commit comments

Comments
 (0)