Jelajahi Sumber

fix a bunch of bugs

00ll00 5 tahun lalu
induk
melakukan
0334ecf286
30 mengubah file dengan 443 tambahan dan 137 penghapusan
  1. 26 49
      src/main/kotlin/bot_luo_core/bot/BotLuo.kt
  2. 3 1
      src/main/kotlin/bot_luo_core/cli/Checker.kt
  3. 10 3
      src/main/kotlin/bot_luo_core/cli/CmdCatalog.kt
  4. 3 3
      src/main/kotlin/bot_luo_core/cli/CmdExecutable.kt
  5. 42 26
      src/main/kotlin/bot_luo_core/cli/CmdHandler.kt
  6. 2 0
      src/main/kotlin/bot_luo_core/cli/CmdParser.kt
  7. 25 0
      src/main/kotlin/bot_luo_core/cli/checkers/GroupDayLimitChecker.kt
  8. 18 0
      src/main/kotlin/bot_luo_core/cli/checkers/addon/CmdParallelExecutingChecker.kt
  9. 4 0
      src/main/kotlin/bot_luo_core/cli/commands/DataCmd.kt
  10. 8 8
      src/main/kotlin/bot_luo_core/cli/commands/HelpCmd.kt
  11. 127 0
      src/main/kotlin/bot_luo_core/cli/commands/StatCmd.kt
  12. 3 1
      src/main/kotlin/bot_luo_core/cli/exceptions/CheckerFatal.kt
  13. 2 1
      src/main/kotlin/bot_luo_core/cli/exceptions/CliException.kt
  14. 0 11
      src/main/kotlin/bot_luo_core/cli/exceptions/CliExceptionLevel.kt
  15. 9 3
      src/main/kotlin/bot_luo_core/cli/exceptions/CliInternalError.kt
  16. 5 0
      src/main/kotlin/bot_luo_core/cli/exceptions/CliTargetException.kt
  17. 6 3
      src/main/kotlin/bot_luo_core/cli/exceptions/CmdExecutingError.kt
  18. 6 3
      src/main/kotlin/bot_luo_core/cli/exceptions/CmdExecutingWarn.kt
  19. 3 1
      src/main/kotlin/bot_luo_core/cli/exceptions/CmdParseFatal.kt
  20. 1 1
      src/main/kotlin/bot_luo_core/cli/handlers/LiteralArgHandler.kt
  21. 2 1
      src/main/kotlin/bot_luo_core/data/CmdDataObj.kt
  22. 14 0
      src/main/kotlin/bot_luo_core/data/Cmds.kt
  23. 19 2
      src/main/kotlin/bot_luo_core/data/Config.kt
  24. 13 7
      src/main/kotlin/bot_luo_core/data/Data.kt
  25. 3 3
      src/main/kotlin/bot_luo_core/data/Group.kt
  26. 1 1
      src/main/kotlin/bot_luo_core/data/User.kt
  27. 7 4
      src/main/kotlin/bot_luo_core/util/Logger.kt
  28. 6 5
      src/main/kotlin/bot_luo_core/util/TableBuilder.kt
  29. 45 0
      src/main/kotlin/bot_luo_core/util/Text.kt
  30. 30 0
      src/main/resources/log4j2.xml

+ 26 - 49
src/main/kotlin/bot_luo_core/bot/BotLuo.kt

@@ -16,6 +16,8 @@ import net.mamoe.mirai.event.*
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.utils.BotConfiguration
+import net.mamoe.mirai.utils.LoggerAdapters
+import net.mamoe.mirai.utils.MiraiLogger
 import org.apache.logging.log4j.Level
 import kotlin.jvm.Throws
 
@@ -40,6 +42,11 @@ object BotLuo {
      */
     private val groupMap = HashMap<Long,ArrayList<Bot>>()
 
+    init {
+        LoggerAdapters.useLog4j2()
+        Runtime.getRuntime().addShutdownHook(Thread{ saveAll() })
+    }
+
     /**
      * ## 登录所有机器人账号
      *
@@ -54,44 +61,6 @@ object BotLuo {
                 protocol = BotConfiguration.MiraiProtocol.ANDROID_PAD
             }
             bot.login()
-
-//            //捕获群员退群(踢出)事件
-//            bot.eventChannel.subscribeAlways<MemberLeaveEvent> {
-//                if (bot.isMainBot(this.groupId)) {
-//                    Logger.receive(this)
-//                    if (this is MemberLeaveEvent.Kick)
-//                        WarnCMD.closeRec(Users.readUser(member.id),operator?.id, Time.time())
-//                    else
-//                        WarnCMD.closeRec(Users.readUser(member.id),null, Time.time())
-//                }
-//            }
-//
-//            //捕获加群事件
-//            bot.eventChannel.subscribeAlways<MemberJoinEvent> {
-//                if (bot.isMainBot(this.groupId)) {
-//                    Logger.receive(this)
-//                    if (group.id == Tags.readGroupTag("#rsc")[0].id) return@subscribeAlways
-//                    //查询Warn记录,若大于3分则在#clip群发送警告
-//                    val user = Users.readUser(this.member.id)
-//                    if (WarnCMD.getScore(user) >= 3) {
-//                        val clipId = Tags.readGroupTag("#clip")[0]
-//                        sendGroupMessage("死人 ${member.id} 想在 ${this.group.name}(${this.group.id}) 复活", clipId.id)
-//                        val env = CmdEnv(null, CmdEnvType.GROUP, Time.time(), clipId, null, User.virtualUser(), CmdParser(StringReader("")))
-//                        WarnCMD(env).get(user,null)
-//                    }
-//                }
-//            }
-//
-//            //捕获加群申请事件
-//            bot.eventChannel.subscribeAlways<MemberJoinRequestEvent> {
-//                if (bot.isMainBot(this.groupId)) {
-//                    Logger.receive(this)
-//                    val group = Groups.readGroup(this.groupId)
-//                    if (group.getCmdStateBoolean(GuardCMD::class.getName(),"auto")) {
-//                        GuardCMD.judge(this)
-//                    }
-//                }
-//            }
         }
 
         buildGroupMap()
@@ -116,11 +85,26 @@ object BotLuo {
             Logger.log(this, Level.INFO)
             buildGroupMap()
         }
+        GlobalEventChannel.subscribeAlways<BotMuteEvent> {
+            Logger.log(this, Level.INFO)
+            buildGroupMap()
+        }
+        GlobalEventChannel.subscribeAlways<BotUnmuteEvent> {
+            Logger.log(this, Level.INFO)
+            buildGroupMap()
+        }
+        GlobalEventChannel.subscribeAlways<MemberJoinRequestEvent> {
+            Logger.log(this, Level.INFO)
+        }
+        GlobalEventChannel.subscribeAlways<MemberJoinEvent> {
+            Logger.log(this, Level.INFO)
+        }
+        GlobalEventChannel.subscribeAlways<MemberLeaveEvent> {
+            Logger.log(this, Level.INFO)
+        }
 
         GlobalEventChannel.subscribeAlways<MessageEvent>(priority = EventPriority.NORMAL) {
             if (this.sender is AnonymousMember) return@subscribeAlways
-            //TODO 解除群锁
-            if (this is GroupAwareMessageEvent && group.id != 565056329L) return@subscribeAlways
             if (this is MessageSyncEvent) return@subscribeAlways
             if (this is GroupAwareMessageEvent && !bot.isMainBotOf(this.group.id)) return@subscribeAlways
             CmdHandler.call(this)
@@ -134,11 +118,6 @@ object BotLuo {
             CmdHandler.call(this)
             if (this.isIntercepted) Logger.log(this, Level.INFO) else Logger.log(this, Level.DEBUG)
         }
-//        GlobalEventChannel.subscribeAlways<MessagePreSendEvent> {
-//            this.cancel()   //TODO 解除张口结舌之术
-//        }
-
-        Runtime.getRuntime().addShutdownHook(Thread{ saveAll() })
     }
 
     /**
@@ -272,10 +251,8 @@ object BotLuo {
     }
 
     fun saveAll() {
-        runBlocking {
-            Data.savingJobs.values.forEach { it.start(); it.join() }
-            Data.savingJobs.keys.forEach { it.cancelAndJoin() }
-        }
+        Data.savingJobs.values.forEach { it.invoke() }
+        Data.savingJobs.clear()
     }
 
 }

+ 3 - 1
src/main/kotlin/bot_luo_core/cli/Checker.kt

@@ -16,7 +16,7 @@ interface Checker {
     fun check(cmd: CmdExecutable, context: CmdContext)
 
     fun CheckerFatal(output: Message, message: String) = CheckerFatal(output, "$name 检定失败:${message}", this::class)
-    fun CheckerFatal(message: String) = CheckerFatal(message.toPlainText(), "$name 检定失败:${message}")
+    fun CheckerFatal(message: String) = CheckerFatal(message.toPlainText(), message)
 
     companion object {
 
@@ -30,7 +30,9 @@ interface Checker {
          */
         val CHECKERS = arrayListOf(
             BotRunningChecker::class,
+            GroupCDChecker::class,
             GroupCmdWorkingChecker::class,
+            GroupDayLimitChecker::class,
             PermissionChecker::class,
             UserCDChecker::class,
             UserCmdWorkingChecker::class,

+ 10 - 3
src/main/kotlin/bot_luo_core/cli/CmdCatalog.kt

@@ -4,6 +4,9 @@ import bot_luo_core.cli.annotation.Method
 import bot_luo_core.cli.commands.*
 import bot_luo_core.cli.exceptions.CliInternalError
 import bot_luo_core.util.Text.escapeRegex
+import java.lang.IllegalArgumentException
+import java.util.*
+import kotlin.collections.ArrayList
 import kotlin.reflect.KClass
 import kotlin.reflect.KFunction
 import kotlin.reflect.full.functions
@@ -12,14 +15,17 @@ import kotlin.reflect.full.hasAnnotation
 object CmdCatalog {
     val COMMANDS = ArrayList<CmdExecutable>()
 
+    /**
+     * 命令头中的非法字符
+     */
     private val ILLEGAL_CHAR = arrayOf('.','*','\"','。','/','\\','[',']','(',')','{','}')
 
     init {
         //注册内建命令
         registerCmd(
             AnnounceCmd::class,
-            DataCmd::class,
             ControlCmd::class,
+            DataCmd::class,
             DebugCmd::class,
             DumpFileCmd::class,
             ExecuteCmd::class,
@@ -27,6 +33,7 @@ object CmdCatalog {
             ListCmd::class,
             PermissionCmd::class,
             SayCmd::class,
+            StatCmd::class,
             SwitchCmd::class,
             TagCmd::class
         )
@@ -42,8 +49,8 @@ object CmdCatalog {
                     @Suppress("UNCHECKED_CAST")
                     val cex = CmdExecutable(cmd, f as KFunction<CmdReceipt>)
 
-                    if (cex.cmdHead.any { it.any { c -> c in ILLEGAL_CHAR } }) throw CliInternalError("命令 $cex 中存在不合法的字符")
-                    if (cex.methHead.any { it.any { c -> c in ILLEGAL_CHAR } }) throw CliInternalError("命令 $cex 中存在不合法的字符")
+                    if (cex.cmdHead.any { it.any { c -> c in ILLEGAL_CHAR } }) throw CliInternalError(IllegalArgumentException("命令 $cex 中存在不合法的字符"))
+                    if (cex.methHead.any { it.any { c -> c in ILLEGAL_CHAR } }) throw CliInternalError(IllegalArgumentException("命令 $cex 中存在不合法的字符"))
 
                     COMMANDS.add(cex)
                 }

+ 3 - 3
src/main/kotlin/bot_luo_core/cli/CmdExecutable.kt

@@ -30,9 +30,9 @@ import kotlin.reflect.full.primaryConstructor
  */
 class CmdExecutable(private val cmd: KClass<out Cmd>, private val meth: KFunction<CmdReceipt>) {
 
-    val cmdAnn: Command by lazy { cmd.findAnnotation()?:throw CliInternalError("命令类 $cmd 未被注解") }
-    val methAnn: Method by lazy { meth.findAnnotation()?:throw CliInternalError("命令类 $cmd 中方法 ${meth.name} 未被注解") }
-    val paramsAnn: Map<KParameter, Argument> by lazy { meth.parameters.filter { it.kind == KParameter.Kind.VALUE }.associateWith { it.findAnnotation()?: throw CliInternalError("命令类 $cmd 方法 ${meth.name} 参数 ${it.name} 未被注解") } }
+    val cmdAnn: Command by lazy { cmd.findAnnotation()?:throw CliInternalError(NullPointerException ("命令类 $cmd 未被注解")) }
+    val methAnn: Method by lazy { meth.findAnnotation()?:throw CliInternalError(NullPointerException ("命令类 $cmd 中方法 ${meth.name} 未被注解")) }
+    val paramsAnn: Map<KParameter, Argument> by lazy { meth.parameters.filter { it.kind == KParameter.Kind.VALUE }.associateWith { it.findAnnotation()?: throw CliInternalError(NullPointerException ("命令类 $cmd 方法 ${meth.name} 参数 ${it.name} 未被注解")) } }
 
     val cmdName = cmdAnn.name
     val cmdHead = listOf(cmdAnn.name, cmdAnn.display, *cmdAnn.alias)

+ 42 - 26
src/main/kotlin/bot_luo_core/cli/CmdHandler.kt

@@ -3,24 +3,21 @@ package bot_luo_core.cli
 import bot_luo_core.bot.VirtualMessageEvent
 import bot_luo_core.cli.Checker.Companion.order
 import bot_luo_core.cli.exceptions.*
+import bot_luo_core.data.*
 import bot_luo_core.data.Config.CMD_PREFIX
 import bot_luo_core.data.Config.MAX_OUTPUT_LEN
-import bot_luo_core.data.Groups
-import bot_luo_core.data.Users
-import bot_luo_core.data.withAccessing
-import bot_luo_core.data.withLockedAccessing
 import bot_luo_core.util.Logger
 import bot_luo_core.util.Text.firstNotWhitespace
-import bot_luo_core.util.Time.isSameDayTo
+import bot_luo_core.util.Text.limitEnd
 import bot_luo_core.util.Time.notSameDayTo
-import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.*
 import net.mamoe.mirai.event.events.GroupAwareMessageEvent
 import net.mamoe.mirai.event.events.MessageEvent
 import net.mamoe.mirai.message.data.At
 import net.mamoe.mirai.message.data.content
 import net.mamoe.mirai.message.data.isContentEmpty
 import net.mamoe.mirai.message.data.toPlainText
+import org.apache.logging.log4j.Level
 import java.lang.reflect.InvocationTargetException
 import kotlin.jvm.Throws
 import kotlin.reflect.full.allSuperclasses
@@ -61,36 +58,47 @@ object CmdHandler {
     /**
      * ## 命令上下文唤起命令
      *
-     * 捕捉命令执行异常和发送回执消息
+     * 捕捉命令执行异常和发送回执消息,有需要时发送命令猜测
      *
-     * 有需要时发送命令猜测
+     * 每次call会新建一个[CoroutineScope]
      */
-    suspend fun call(context: CmdContext) {
+    private suspend fun call(context: CmdContext) { CoroutineScope(Dispatchers.IO).launch {
         try {
-            withAccessing(context.user, context.group) {
+            withAccessing(context.user, context.group, Cmds) {
                 execute(context)
             }
         } catch (e: Exception) {
-            when {
-                e::class.simpleName in context.groupF.mutedExceptions ||
-                        e::class.allSuperclasses.any { it.simpleName in context.groupF.mutedExceptions } -> {}
-                e is CheckerFatal && e.checker.simpleName in context.groupF.mutedCheckers -> {}
-                e is CliException && !e.output.isContentEmpty() -> {
-                    context.sendOutputWithLog(At(context.user.id) + e.output)
+            if (e is CliException) {
+                when {
+                    e::class.simpleName in context.groupF.mutedExceptions ||
+                            e::class.allSuperclasses.any { it.simpleName in context.groupF.mutedExceptions } -> {
+                    }
+                    e is CliTargetException && (e.targetException::class.simpleName in context.groupF.mutedExceptions ||
+                            e.targetException::class.allSuperclasses.any { it.simpleName in context.groupF.mutedExceptions }) -> {
+                    }
+                    e is CheckerFatal && e.checker.simpleName in context.groupF.mutedCheckers -> {
+                    }
+                    else -> {
+                        if (!e.output.isContentEmpty())
+                            context.sendOutputWithLog(At(context.user.id) + e.output)
+                    }
                 }
-                else -> context.sendOutputWithLog("发生异常:${e::class.simpleName}".toPlainText())
+                Logger.cliLog(e.logLevel, e.message)
+            } else {
+                context.sendOutputWithLog("发生未处理异常:${e::class.simpleName}".toPlainText())
+                Logger.cliError(e, Level.ERROR)
             }
-            return
+            return@launch
         }
         when (context.uploadOutputFile) {
-            0 -> {
+            0 -> {  //不自动上传
                 if (context.getOutput().content.length > MAX_OUTPUT_LEN) {
-                    context.sendOutputWithLog("输出过大,考虑使用dumpfile命令获取输出")
+                    context.sendOutputWithLog(context.getOutput().limitEnd(MAX_OUTPUT_LEN) + "\n输出过大,考虑使用dumpfile命令获取输出")
                 } else {
                     context.sendOutputWithLog()
                 }
             }
-            1 -> {
+            1 -> {  //长度超出限制时上传
                 if (context.getOutput().content.length > MAX_OUTPUT_LEN) {
                     val receipt = context.uploadOutput()
                     if (receipt == null) {
@@ -100,14 +108,14 @@ object CmdHandler {
                     context.sendOutputWithLog()
                 }
             }
-            2 -> {
+            2 -> {  //始终上传
                 val receipt = context.uploadOutput()
                 if (receipt == null) {
                     context.sendOutputWithLog("输出上传文件失败")
                 }
             }
         }
-    }
+    } }
 
     /**
      * ## 执行命令
@@ -138,7 +146,7 @@ object CmdHandler {
 
                     Logger.log(cmd, context, "执行完成:${res.success}")
 
-                    runBlocking { withLockedAccessing(context.user, context.group) {
+                    runBlocking { withLockedAccessing(context.user, context.group, Cmds) {
                         if (res.addCount) {
                             val ud = context.user.readCmdData(cmd)
                             ud.totalCount++
@@ -150,6 +158,11 @@ object CmdHandler {
                             if (time notSameDayTo gd.lastTime) gd.dayCount = 0
                             gd.dayCount++
                             context.group.writeCmdData(cmd, gd)
+                            val ad = Cmds.readCmdData(cmd)
+                            ad.totalCount++
+                            if (time notSameDayTo ad.lastTime) ad.dayCount = 0
+                            ad.dayCount++
+                            Cmds.writeCmdData(cmd, ad)
                         }
 
                         if (res.setTime) {
@@ -159,6 +172,9 @@ object CmdHandler {
                             val gd = context.group.readCmdData(cmd)
                             gd.lastTime = context.time
                             context.group.writeCmdData(cmd, gd)
+                            val ad = Cmds.readCmdData(cmd)
+                            ad.lastTime = context.time
+                            Cmds.writeCmdData(cmd, ad)
                         }
                     } }
 
@@ -174,7 +190,7 @@ object CmdHandler {
                                 else -> throw CmdExecutingError(e.targetException)
                             }
                         }
-                        else -> throw CliInternalError("调用命令时发送错误:\n${e::class.simpleName}".toPlainText(), "调用命令时发送错误:\n${e::class.simpleName}\n${e.message}")
+                        else -> throw CliInternalError(e)
                     }
                 } finally {
                     context.user.cmdFinished(cmd)

+ 2 - 0
src/main/kotlin/bot_luo_core/cli/CmdParser.kt

@@ -12,6 +12,8 @@ import kotlin.reflect.full.createInstance
 
 /**
  * # 命令解析器类
+ *
+ * @throws NoCmdFound 无匹配命令
  */
 class CmdParser(private val reader: MessageReader) {
 

+ 25 - 0
src/main/kotlin/bot_luo_core/cli/checkers/GroupDayLimitChecker.kt

@@ -0,0 +1,25 @@
+package bot_luo_core.cli.checkers
+
+import bot_luo_core.cli.Checker
+import bot_luo_core.cli.CmdContext
+import bot_luo_core.cli.CmdExecutable
+import bot_luo_core.util.Time.isSameDayTo
+import net.mamoe.mirai.message.data.toPlainText
+
+class GroupDayLimitChecker: Checker {
+
+    override val name = "群组当日次数检定器"
+
+    override fun check(cmd: CmdExecutable, context: CmdContext) { with(context) {
+        val limit = group.getObj<Long>("${cmd.id}_groupDayLimit")?: 0
+        val data = group.readCmdData(cmd)
+        if (limit > 0) {
+            if (time isSameDayTo data.lastTime) {
+                if (data.dayCount >= limit) throw CheckerFatal(
+                    "恁改天吧".toPlainText(),
+                    "达到群组当日次数限制 $limit"
+                )
+            }
+        }
+    } }
+}

+ 18 - 0
src/main/kotlin/bot_luo_core/cli/checkers/addon/CmdParallelExecutingChecker.kt

@@ -0,0 +1,18 @@
+package bot_luo_core.cli.checkers.addon
+
+import bot_luo_core.cli.Checker
+import bot_luo_core.cli.CmdContext
+import bot_luo_core.cli.CmdExecutable
+import net.mamoe.mirai.message.data.toPlainText
+
+class CmdParallelExecutingChecker: Checker {
+    override val name = "全局命令并发检定器"
+
+    override fun check(cmd: CmdExecutable, context: CmdContext) {
+        if (!context.user.isCmdFree(cmd)) throw CheckerFatal(
+            "您呼叫的命令正忙,请稍后再试".toPlainText(),
+            "全局有相同命令正在执行"
+        )
+    }
+
+}

+ 4 - 0
src/main/kotlin/bot_luo_core/cli/commands/DataCmd.kt

@@ -85,6 +85,7 @@ class DataCmd(context: CmdContext) : Cmd(context) {
         table.th("写入${objName}数据 —— $objId")
         table.p("/"+path.joinToString("/")).br().br()
         try { withLockedAccessing(dataObject) {
+            dataObject.markDirty()
             val (old, new) = writeByPathTyped(dataObject.jsonObj, path, defaultCast(value))
             table.tr(formatValue(old)).tb("->").tb(formatValue(new))
             context.print(table.toString())
@@ -103,6 +104,7 @@ class DataCmd(context: CmdContext) : Cmd(context) {
         table.th("写入${arrayName}数据 —— $arrayId")
         table.p("/"+path.joinToString("/")).br().br()
         try { withLockedAccessing(dataArray) {
+            dataArray.markDirty()
             val (old, new) = writeByPathTyped(dataArray.jsonArray, path, defaultCast(value))
             table.tr(formatValue(old)).tb("->").tb(formatValue(new))
             context.print(table.toString())
@@ -121,6 +123,7 @@ class DataCmd(context: CmdContext) : Cmd(context) {
         table.th("写入${objName}数据 —— $objId")
         table.p("/"+path.joinToString("/")).br().br()
         try { withLockedAccessing(dataObject) {
+            dataObject.markDirty()
             val (old, new) = writeByPathTyped(dataObject.jsonObj, path, value castTo type)
             table.tr(formatValue(old)).tb("->").tb(formatValue(new))
             context.print(table.toString())
@@ -139,6 +142,7 @@ class DataCmd(context: CmdContext) : Cmd(context) {
         table.th("写入${arrayName}数据 —— $arrayId")
         table.p("/"+path.joinToString("/")).br().br()
         try { withLockedAccessing(dataArray) {
+            dataArray.markDirty()
             val (old, new) = writeByPathTyped(dataArray.jsonArray, path, value castTo type)
             table.tr(formatValue(old)).tb("->").tb(formatValue(new))
             context.print(table.toString())

+ 8 - 8
src/main/kotlin/bot_luo_core/cli/commands/HelpCmd.kt

@@ -94,18 +94,18 @@ class HelpCmd(context: CmdContext) : Cmd(context) {
                 table.th("帮助 —— 命令($k)[${v[0].cmdHead.joinToString(",")}]").br()
                 table.p(v[0].usage).br().br()
                 map.forEach { (_ ,value ) ->
-                    table.prettyLines(value[0].subTitle, value.size) { index, builder ->
-                        builder.tb(value[index].headPara).tb(value[index].getParasFormat())
+                    table.prettyLines(value[0].subTitle, value) { item, builder ->
+                        builder.tb(item.headPara).tb(item.getParasFormat())
                     }.tb(value[0].subUsage)
                 }
-                table.th().prettyLines("示例:",spl.size) { index, builder ->
-                    builder.tb(spl[index])
+                table.th().prettyLines("示例:",spl) { item, builder ->
+                    builder.tb(item)
                 }
-                table.th().prettyLines("说明:",v[0].caption.size) { index, builder ->
-                    builder.tb(v[0].caption[index])
+                table.th().prettyLines("说明:",v[0].caption.toList()) { item, builder ->
+                    builder.tb(item)
                 }
-                table.th().prettyLines("注意:",v[0].notice.size) { index, builder ->
-                    builder.tb(v[0].notice[index])
+                table.th().prettyLines("注意:",v[0].notice.toList()) { item, builder ->
+                    builder.tb(item)
                 }
             }
             return table.toString()

+ 127 - 0
src/main/kotlin/bot_luo_core/cli/commands/StatCmd.kt

@@ -0,0 +1,127 @@
+package bot_luo_core.cli.commands
+
+import bot_luo_core.cli.*
+import bot_luo_core.cli.CmdCatalog.COMMANDS
+import bot_luo_core.cli.annotation.Argument
+import bot_luo_core.cli.annotation.Command
+import bot_luo_core.cli.annotation.Method
+import bot_luo_core.cli.checkers.GroupCmdWorkingChecker
+import bot_luo_core.cli.checkers.UserCmdWorkingChecker
+import bot_luo_core.cli.checkers.addon.GroupPermissionChecker
+import bot_luo_core.cli.exceptions.CheckerFatal
+import bot_luo_core.cli.handlers.CmdExArgHandler
+import bot_luo_core.data.CmdDataObj
+import bot_luo_core.data.Cmds
+import bot_luo_core.util.TableBuilder
+import bot_luo_core.util.Time.relativeToE
+import bot_luo_core.util.Time.spanFromE
+import net.mamoe.mirai.message.data.MessageChain
+import kotlin.reflect.full.createInstance
+
+@Command(
+    name = "stat",
+    display = "统计",
+    alias = [],
+    usage = "查看罗伯特业绩"
+)
+class StatCmd(context: CmdContext) : Cmd(context) {
+
+    suspend fun getDetail(carrier: CmdDataObj, name: String, id: String, cmds: List<CmdExecutable>): CmdReceipt {
+        val filtered = cmds.filter {
+            try {
+                justCheck.forEach { checker -> if (checker in it.checkers) checker.createInstance().check(it, context) }
+                true
+            } catch (ignore: CheckerFatal) {
+                false
+            }
+        }
+
+        return if (filtered.isEmpty()) {
+            context.sendMessageWithLog("${name}统计 —— 403 Forbidden")
+            FATAL
+        } else {
+            val table = TableBuilder(4)
+            filtered.groupBy { it.cmdName }.forEach { (k, v) ->
+                table.th("${name}统计 —— $id").br()
+                table.prettyLines(
+                    "${k}:${v.sumOf { carrier.readCmdData(it).totalCount }}次",
+                    v.groupBy { it.methName }.entries
+                ) { (key, value), builder ->
+                    builder.tb("-$key:")
+                        .tb("${value.sumOf { carrier.readCmdData(it).totalCount }}次")
+                        .tb(value.maxOf { carrier.readCmdData(it).lastTime } relativeToE context.time)
+                }
+            }
+            context.sendMessageWithLog(table.toString())
+            SUCCESS
+        }
+    }
+
+    suspend fun getGeneral(carrier: CmdDataObj, name: String, id: String, cmds: ArrayList<CmdExecutable>): CmdReceipt {
+        val filtered = cmds.filter {
+            try {
+                justCheck.forEach { checker -> if (checker in it.checkers) checker.createInstance().check(it, context) }
+                true
+            } catch (ignore: CheckerFatal) {
+                false
+            }
+        }
+
+        return if (filtered.isEmpty()) {
+            context.sendMessageWithLog("${name}统计 —— 403 Forbidden")
+            FATAL
+        } else {
+            val table = TableBuilder(4)
+            table.th("${name}统计 —— $id").br()
+            filtered.groupBy { it.cmdName }.forEach { (k, v) ->
+                table.tr("$k:")
+                    .tb("${v.sumOf { carrier.readCmdData(it).totalCount }}次")
+                    .tb(v.maxOf { carrier.readCmdData(it).lastTime } relativeToE context.time)
+            }
+            context.sendMessageWithLog(table.toString())
+            SUCCESS
+        }
+    }
+
+    /*  ========================  cmd  ========================  */
+
+    @Method(name = "", alias = ["cmd", "c"], pmsLevel = CmdPermissionLevel.HIGH, title = "全局统计", order = 0)
+    suspend fun cmd (
+        @Argument(name = "命令", handler = CmdExArgHandler::class)
+        cmds: List<CmdExecutable>
+    ): CmdReceipt = getDetail(Cmds, "全局命令", "", cmds)
+
+    @Method(name = "", alias = ["cmd", "c"], pmsLevel = CmdPermissionLevel.HIGH, title = "全局统计", order = 1)
+    suspend fun cmd (): CmdReceipt = getGeneral(Cmds, "全局命令", "", COMMANDS)
+
+    /*  ========================  group  ========================  */
+
+    @Method(name = "group", alias = ["g"], pmsLevel = CmdPermissionLevel.HIGH, title = "群组统计", order = 0)
+    suspend fun group (
+        @Argument(name = "命令", handler = CmdExArgHandler::class)
+        cmds: List<CmdExecutable>
+    ): CmdReceipt = getDetail(context.group, "群组", "${context.group.name}(${context.group.id})", cmds)
+
+    @Method(name = "group", alias = ["g"], pmsLevel = CmdPermissionLevel.HIGH, title = "群组统计", order = 1)
+    suspend fun group (): CmdReceipt = getGeneral(context.group, "群组", "${context.group.name}(${context.group.id})", COMMANDS)
+
+    /*  ========================  user  ========================  */
+
+    @Method(name = "user", alias = ["u"], pmsLevel = CmdPermissionLevel.HIGH, title = "用户统计", order = 0)
+    suspend fun user (
+        @Argument(name = "命令", handler = CmdExArgHandler::class)
+        cmds: List<CmdExecutable>
+    ): CmdReceipt = getDetail(context.user, "用户", "${context.user.name}(${context.user.id})", cmds)
+
+    @Method(name = "user", alias = ["u"], pmsLevel = CmdPermissionLevel.HIGH, title = "用户统计", order = 1)
+    suspend fun user (): CmdReceipt = getGeneral(context.user, "用户", "${context.user.name}(${context.user.id})", COMMANDS)
+
+    companion object {
+
+        val justCheck = arrayOf(
+            GroupPermissionChecker::class,
+            GroupCmdWorkingChecker::class,
+            UserCmdWorkingChecker::class
+        )
+    }
+}

+ 3 - 1
src/main/kotlin/bot_luo_core/cli/exceptions/CheckerFatal.kt

@@ -2,6 +2,8 @@ package bot_luo_core.cli.exceptions
 
 import bot_luo_core.cli.Checker
 import net.mamoe.mirai.message.data.Message
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.Level.INFO
 import kotlin.reflect.KClass
 
 /**
@@ -12,5 +14,5 @@ class CheckerFatal(
     override val message: String,
     val checker: KClass<out Checker>
     ) : CliException() {
-    override val level = CliExceptionLevel.INFO
+    override val logLevel: Level = INFO
 }

+ 2 - 1
src/main/kotlin/bot_luo_core/cli/exceptions/CliException.kt

@@ -1,6 +1,7 @@
 package bot_luo_core.cli.exceptions
 
 import net.mamoe.mirai.message.data.Message
+import org.apache.logging.log4j.Level
 
 /**
  * # CLI异常基类
@@ -11,7 +12,7 @@ abstract class CliException: Exception() {
     /**
      * 等级
      */
-    abstract val level: CliExceptionLevel
+    abstract val logLevel: Level
 
     /**
      * 输出消息,用于错误信息

+ 0 - 11
src/main/kotlin/bot_luo_core/cli/exceptions/CliExceptionLevel.kt

@@ -1,11 +0,0 @@
-package bot_luo_core.cli.exceptions
-
-enum class CliExceptionLevel {
-    /**
-     * info是纯粹的返回信息,不会在log中打印堆栈
-     */
-    INFO,
-    WARN,
-    ERROR,
-    DEBUG
-}

+ 9 - 3
src/main/kotlin/bot_luo_core/cli/exceptions/CliInternalError.kt

@@ -2,12 +2,18 @@ package bot_luo_core.cli.exceptions
 
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.toPlainText
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.Level.ERROR
 
 /**
  * 内部错误,写出bug了
  */
-class CliInternalError(override val output: Message, override val message: String) : CliException() {
-    override val level = CliExceptionLevel.ERROR
+class CliInternalError(override val output: Message, override val message: String, override val targetException: Throwable) : CliException(), CliTargetException {
+    override val logLevel: Level = ERROR
 
-    constructor(msg: String): this(msg.toPlainText(), msg)
+    constructor(e: Throwable): this(
+        "调用命令时发送错误:\n${e::class.simpleName}".toPlainText(),
+        "调用命令时发送错误:\n${e::class.simpleName}: ${e.message}\n${e.stackTraceToString()}",
+        e
+    )
 }

+ 5 - 0
src/main/kotlin/bot_luo_core/cli/exceptions/CliTargetException.kt

@@ -0,0 +1,5 @@
+package bot_luo_core.cli.exceptions
+
+interface CliTargetException {
+    val targetException: Throwable
+}

+ 6 - 3
src/main/kotlin/bot_luo_core/cli/exceptions/CmdExecutingError.kt

@@ -2,15 +2,18 @@ package bot_luo_core.cli.exceptions
 
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.toPlainText
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.Level.ERROR
 
 /**
  * 命令执行错误,通常由执行命令时接受到命令内部抛出的非[CliException]错误转换得到
  */
-class CmdExecutingError(override val output: Message, override val message: String) : CliException() {
-    override val level = CliExceptionLevel.ERROR
+class CmdExecutingError(override val output: Message, override val message: String, override val  targetException: Throwable) : CliException(), CliTargetException {
+    override val logLevel: Level = ERROR
 
     constructor(e: Throwable): this (
         "命令执行中发生错误:\n${e::class.simpleName}".toPlainText(),
-        "[ERROR]发生错误:${e.message}"
+        "发生错误:${e::class.simpleName}: ${e.message}\n${e.stackTraceToString()}",
+        e
             )
 }

+ 6 - 3
src/main/kotlin/bot_luo_core/cli/exceptions/CmdExecutingWarn.kt

@@ -2,15 +2,18 @@ package bot_luo_core.cli.exceptions
 
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.toPlainText
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.Level.WARN
 
 /**
  * 命令执行错误,通常由执行命令时接受到命令内部抛出的非[CliException]错误转换得到
  */
-class CmdExecutingWarn(override val output: Message, override val message: String) : CliException() {
-    override val level = CliExceptionLevel.WARN
+class CmdExecutingWarn(override val output: Message, override val message: String, override val targetException: Throwable) : CliException(), CliTargetException {
+    override val logLevel: Level = WARN
 
     constructor(e: Throwable): this (
         "命令执行中发生意外:\n${e::class.simpleName}".toPlainText(),
-        "[WARN]发生错误:${e.message}"
+        "发生错误:${e::class.simpleName}: ${e.message}\n${e.stackTraceToString()}",
+        e
     )
 }

+ 3 - 1
src/main/kotlin/bot_luo_core/cli/exceptions/CmdParseFatal.kt

@@ -3,13 +3,15 @@ package bot_luo_core.cli.exceptions
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.MessageChainBuilder
 import net.mamoe.mirai.message.data.toPlainText
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.Level.INFO
 import kotlin.reflect.KType
 
 /**
  * 命令行解析错误
  */
 abstract class CmdParseFatal: CliException() {
-    override val level = CliExceptionLevel.INFO
+    override val logLevel: Level = INFO
 
     abstract val pos: Int
 }

+ 1 - 1
src/main/kotlin/bot_luo_core/cli/handlers/LiteralArgHandler.kt

@@ -12,7 +12,7 @@ class LiteralArgHandler: ArgHandler<Unit> {
     override val name = "字面量参数解析器"
 
     override fun handle(reader: MessageReader, pos: Int, argName: String?, type: KType?, context: CmdContext?) {
-        argName?: throw CliInternalError("缺少字面量名称")
+        argName?: throw CliInternalError(NullPointerException ("缺少字面量名称"))
         val input = reader.readUnquotedString()
         if (input != argName) throw HandlerFatal(input, argName, pos, type)
     }

+ 2 - 1
src/main/kotlin/bot_luo_core/data/CmdDataObj.kt

@@ -4,7 +4,8 @@ import bot_luo_core.cli.CmdExecutable
 import kotlinx.atomicfu.AtomicInt
 import kotlinx.atomicfu.atomic
 
-abstract class CmdDataObj(filePath: String): DataObject(filePath, 3000, true) {
+abstract class CmdDataObj(filePath: String, autoSaveInterval: Long, saveAndUnload: Boolean):
+    DataObject(filePath, autoSaveInterval, saveAndUnload) {
 
     private val cmdData: HashMap<String, CmdData> =  getObj("cmdData")?: HashMap()
     private val cmdHandling = HashMap<CmdExecutable, AtomicInt>()

+ 14 - 0
src/main/kotlin/bot_luo_core/data/Cmds.kt

@@ -0,0 +1,14 @@
+package bot_luo_core.data
+
+import bot_luo_core.cli.CmdExecutable
+
+object Cmds: CmdDataObj("data/cmds.json", 10000L, false) {
+    override val defaultData get() = CmdData(0, 0, 0, false)
+
+    override fun unload() {}
+
+    operator fun get(id: String): CmdData = readCmdData(id)
+    operator fun get(cmd: CmdExecutable) = readCmdData(cmd)
+    operator fun set(id: String, value: CmdData) = writeCmdData(id, value)
+    operator fun set(cmd: CmdExecutable, value: CmdData) = writeCmdData(cmd, value)
+}

+ 19 - 2
src/main/kotlin/bot_luo_core/data/Config.kt

@@ -4,7 +4,7 @@ object Config {
     /**
      * 命令前缀
      */
-    val CMD_PREFIX = arrayOf('.','。','/')
+    val CMD_PREFIX = arrayOf('.','。')
 
     /**
      * 回复-确认
@@ -19,5 +19,22 @@ object Config {
     /**
      * 输出消息最大长度
      */
-    const val MAX_OUTPUT_LEN = 1000
+    const val MAX_OUTPUT_LEN = 1500
+
+    /**
+     * 默认静音的[Exception]
+     */
+    val MUTED_EXCEPTIONS = arrayListOf(
+        "NoCmdFound"
+    )
+
+    /**
+     * 默认静音的[bot_luo_core.cli.Checker]
+     */
+    val MUTED_CHECKERS = arrayListOf(
+        "PermissionChecker",
+        "GroupCmdWorkingChecker",
+        "UserCmdWorkingChecker",
+        "BotRunningChecker"
+    )
 }

+ 13 - 7
src/main/kotlin/bot_luo_core/data/Data.kt

@@ -55,7 +55,7 @@ abstract class Data(
     fun access() {
         accessing.addAndGet(1)
         saveJob?.cancel()
-        savingJobs.remove(saveJob)
+        savingJobs.remove(this)
         saveJob = null
     }
 
@@ -76,19 +76,18 @@ abstract class Data(
      */
     protected fun launchSaveJob() {
         saveJob?.cancel()
-        savingJobs.remove(saveJob)
+        savingJobs.remove(this)
         saveJob = scope.launch {
             try {
                 delay(autoSaveInterval)
             } catch (ignore: CancellationException) {
                 return@launch
             }
-            if (changed) savingJobs[saveJob]?.start()
+            if (changed) savingJobs[this@Data]?.invoke()
             if (saveAndUnload) unload()
-        }.apply {
-            savingJobs[this] = scope.launch(start = CoroutineStart.LAZY) { save() }
-            this.invokeOnCompletion { savingJobs.remove(this) }
+            savingJobs.remove(this@Data)
         }
+        savingJobs[this] =  { save() }
     }
 
     abstract fun save()
@@ -106,9 +105,16 @@ abstract class Data(
     fun exists() = File(filePath).exists()
 
     companion object {
-        val savingJobs = HashMap<Job, Job>()
+        val savingJobs = HashMap<Data, () -> Unit>()
         val scope = CoroutineScope(Dispatchers.IO)
     }
+
+    /**
+     * 手动标记这个[Data]被修改过,需要保存
+     */
+    fun markDirty() {
+        changed = true
+    }
 }
 
 /**

+ 3 - 3
src/main/kotlin/bot_luo_core/data/Group.kt

@@ -16,7 +16,7 @@ import net.mamoe.mirai.contact.MemberPermission
 class Group internal constructor(
     override val id: Long,
     override var bot: Bot? = null
-) : CmdDataObj("data/groups/$id.json"), BotContact {
+) : CmdDataObj("data/groups/$id.json", 30000L, true), BotContact {
     override val contactType = BotContactType.Group
     override val defaultData: CmdData
         get() = CmdData(0,0,0,false)//群组命令默认关闭
@@ -41,11 +41,11 @@ class Group internal constructor(
         set(value) = setObj("pmsGroup", value.name)
 
     var mutedExceptions: ArrayList<String>
-        get() = getObj("mutedExceptions")?: ArrayList()
+        get() = getObj("mutedExceptions")?: Config.MUTED_EXCEPTIONS
         set(value) = setObj("mutedExceptions", value)
 
     var mutedCheckers: ArrayList<String>
-        get() = getObj("mutedCheckers")?: ArrayList()
+        get() = getObj("mutedCheckers")?: Config.MUTED_CHECKERS
         set(value) = setObj("mutedCheckers", value)
 
     inline fun withServeBot(pms: MemberPermission = MemberPermission.MEMBER, action: (contact: Group)->Unit): Boolean {

+ 1 - 1
src/main/kotlin/bot_luo_core/data/User.kt

@@ -17,7 +17,7 @@ import net.mamoe.mirai.message.data.At
 class User internal constructor (
     override val id: Long,
     override var bot: Bot? = null
-) : CmdDataObj("data/users/$id.json"), BotContact {
+) : CmdDataObj("data/users/$id.json", 30000L, true), BotContact {
     override val contactType = BotContactType.Single
     override val defaultData: CmdData get() = CmdData(0,0,0,true)//用户命令默认开启
     override var contact: Contact? = null

+ 7 - 4
src/main/kotlin/bot_luo_core/util/Logger.kt

@@ -20,10 +20,10 @@ import org.apache.logging.log4j.LogManager
 
 object Logger {
 
-    private val sysLogger = LogManager.getLogger("SYS")
-    private val msgLogger = LogManager.getLogger("MSG")
-    private val eventLogger = LogManager.getLogger("EVENT")
-    private val cliLogger = LogManager.getLogger("CLI")
+    private val sysLogger = LogManager.getLogger("Sys")
+    private val msgLogger = LogManager.getLogger("Msg")
+    private val eventLogger = LogManager.getLogger("Event")
+    private val cliLogger = LogManager.getLogger("Cli")
 
     /*  ========================  SYS  ========================  */
 
@@ -124,4 +124,7 @@ object Logger {
 
     fun log(cmd: CmdExecutable, info: String, level: Level = Level.INFO) =
         cliLog(level, "<${cmd.id}> $info")
+
+    fun cliError(e: Throwable, level: Level) =
+        cliLog(level, e::class.simpleName + ": " + e.message + "\n" + e.stackTraceToString())
 }

+ 6 - 5
src/main/kotlin/bot_luo_core/util/TableBuilder.kt

@@ -114,18 +114,19 @@ class TableBuilder (
         y = -1
     }
 
-    fun prettyLines(title: String, lines: Int, action: (index: Int, builder: TableBuilder)->Unit): TableBuilder {
-        when (lines) {
+    fun <T> prettyLines(title: String, content: Iterable<T>, action: (item: T, builder: TableBuilder)->Unit): TableBuilder {
+        when (content.count()) {
             0 -> {}
             1 -> {
                 tr(title)
-                action(0, this)
+                action(content.elementAt(0), this)
             }
             else -> {
                 tr(title)
-                for (i in 0 until lines) {
+                val iterator = content.iterator()
+                while (iterator.hasNext()) {
                     tr("")
-                    action(i, this)
+                    action(iterator.next(), this)
                 }
             }
         }

+ 45 - 0
src/main/kotlin/bot_luo_core/util/Text.kt

@@ -1,8 +1,14 @@
 package bot_luo_core.util
 
 import com.alibaba.fastjson.JSON
+import com.sun.deploy.util.StringUtils
 import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageChainBuilder
+import net.mamoe.mirai.message.data.PlainText
 import net.mamoe.mirai.message.data.content
+import java.net.URLDecoder
+import java.net.URLEncoder
 import java.util.*
 
 object Text {
@@ -19,12 +25,14 @@ object Text {
     fun String.escapeJson(): String = JSON.toJSONString(this@escapeJson)
 
     infix operator fun String.times(n: Int): String {
+        if (n<=0) return ""
         val sb = StringBuilder()
         repeat(n) { sb.append(this) }
         return sb.toString()
     }
 
     infix operator fun Char.times(n: Int): String {
+        if (n<=0) return ""
         val sb = StringBuilder()
         repeat(n) { sb.append(this) }
         return sb.toString()
@@ -43,4 +51,41 @@ object Text {
      * 将此字符串视为MiraiCode进行解码并获取其内容
      */
     val String.miraiCodeContent: String get() = this.deserializeMiraiCode().content
+
+    fun String.encodeUrl(): String = URLEncoder.encode(this, "UTF-8")
+    fun String.decodeUrl(): String = URLDecoder.decode(this, "UTF-8")
+
+    /**
+     * 限制总长,当
+     */
+    fun String.limitEnd(len: Int): String {
+        if (len <= 3) return '.'*len
+        return if (this.length > len)
+            this.substring(0..len-4) + "..."
+        else
+            this
+    }
+
+    fun String.limitMid(len: Int): String {
+        if (len <= 3) return '.'*len
+        return if (this.length > len)
+            this.substring(0 until  (len-3)/2) + "..." + this.substring(length - (len-3)/2 until length )
+        else
+            this
+    }
+
+    fun MessageChain.limitEnd(len: Int): MessageChain {
+        val mcb = MessageChainBuilder()
+        var l = 0
+        for (m in this) {
+            l += m.content.length
+            if (l > len) {
+                if (m is PlainText) {
+                    mcb.add(m.content.limitEnd(len - l + m.content.length))
+                }
+                break
+            }
+        }
+        return mcb.build()
+    }
 }

+ 30 - 0
src/main/resources/log4j2.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+    <Appenders>
+        <Console name="SysOut" target="SYSTEM_OUT">
+            <PatternLayout pattern="%highlight{[%d{HH:mm:ss}] [%logger{1}/%level]: %msg%n}{STYLE=Logback}" disableAnsi="false"/>
+        </Console>
+        <RollingRandomAccessFile name="LogFile" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
+            <PatternLayout pattern="[%d{HH:mm:ss}] [%logger/%level]: %msg%n" />
+            <ThresholdFilter level="info"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy />
+            </Policies>
+        </RollingRandomAccessFile>
+        <RollingRandomAccessFile name="DebugFile" fileName="logs/debug-0.log" filePattern="logs/debug-%i.log">
+            <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%logger/%level]: %msg%n" />
+            <Policies>
+                <SizeBasedTriggeringPolicy size="1MB"/>
+                <OnStartupTriggeringPolicy />
+            </Policies>
+            <DefaultRolloverStrategy max="4"/>
+        </RollingRandomAccessFile>
+    </Appenders>
+    <Loggers>
+        <Root level="all">
+            <AppenderRef ref="SysOut"/>
+            <AppenderRef ref="LogFile"/>
+            <AppenderRef ref="DebugFile"/>
+        </Root>
+    </Loggers>
+</Configuration>