指令管理的优化建议

来自 2024 年的回旋镖:

  • 下个版本将支持禁用原生别名的功能(你甚至可以把所有名字全删了)
  • 关于上面提到的无法触发的问题,会采用「原名可以被插件调用但不能被用户触发」的处理思路
7 个赞

快把香槟全部开了!
对了,如果原名可以被插件调用的话,那要怎么解决冲突的问题呢

4 个赞

倆打一架,誰贏給誰

4 个赞

原名不能冲突,别名也不能冲突。

原名无法修改,因此两个插件的指令如果原名相同,那么就只能启用一个。

别名可以禁用或者修改,因此两个插件的指令如果别名相同(但是原名不同),你可以先启用一个插件,然后把冲突的别名禁用掉,再启用另一个插件。

4 个赞

原名不能冲突只是为了session.execute吗,还是有其他更深层的原因

2 个赞

无论是原名不能冲突还是别名不能冲突,都是因为如果两个插件别名冲突那么同时安装行为会不确定。

Koishi 希望相同的配置文件产生相同的行为。

在此之上,原名不能冲突有着更多意义:

  1. 如果原名冲突,那么无法基于 session.execute 实现调用指令
  2. 如果原名冲突,那么无法在本地化中自定义输出文本
  3. 如果原名冲突,那么无法在指令管理中设置权限等属性

原名是指令的唯一标识符,如果冲突了一切基于此的设置都做不了。

6 个赞

明白了,谢谢海胆

3 个赞

明白了,谢谢海胆

3 个赞

明白了,谢谢海胆

2 个赞

已更新:

5 个赞

我宁愿同时触发多个指令,把选项、内容都传入每一个注册的指令,对每一个指令都进行权限的判断。

Shigma 提到的都是一些技术上的问题,也许积重难返不好更改了。

3 个赞

这样就会导致:

依据你的愿望设想以下场景

场景 1

  • 插件 A 注册了指令 foo,返回 乌拉!,插件 B 同样注册了指令 foo,返回 foofoo
  • 此时,插件 A 与 B 同时启动,并且有一个插件 C 通过 session.execute 调用了指令 foo 并期待返回 乌拉!

你觉得该怎么办呢?

场景 2

  • 这是插件 A 的本地化定义:
foo:
 messages:
   return: 乌拉!
  • 这是插件B 的本地化定义:
foo:
 messages:
   return: foofoo

如果在同时安装并启用了插件 A 与 B 情况下调用了指令 foo,应该返回哪一个?

场景 3

当某个地方禁止发送 乌拉!,Bot 维护者为了机器人的安全,在同时安装并启用了插件 A 与 B 情况下试图禁止因为 foo 而触发 乌拉!,但是禁止了 foo 指令之后 foofoo 也一起不允许了,该怎么办呢?


这不是技术上的问题,也不是积重难返不好更改,指令就应当是唯一的

双胞胎长得再一样也得有不一样的身份证号码,这里的指令就是这个身份证号码。

当两个插件的指令重复时最佳解决方案是两个开发者打一架,谁赢算谁的。

6 个赞

这个比喻不太恰当,因为一个国家内也有很多重名的人,人名是人名,身份证号是身份证号。

现在的做法就相当于国家给国民任选身份证号,谁不想有个精巧的身份证号呢?

同一个插件能有多份配置,同一个事件名能有多个 listener,同一个指令名为什么不能有多个 action ?

可以给指令相关的函数如 ctx.command session.execute 都提供一个 multiple 选项。比如对于 execute 如果使用了这个选项则执行所有同名指令并返回一个结果数组;如果没有这个选项则执行第一个注册或最后一个注册的指令;如果想要执行某个特定的指令,应找出它的 ID。

在控制台的指令管理界面,每个指令指出定义它的多个插件,对于同名的多个指令,可以独立地对其中某个指令进行禁用、权限管理、本地化等。

这样对你提到的每个场景都有解决方法。

3 个赞

这个设计很愚蠢。 唯一标识符应具有不易发生冲突的属性。

愚蠢的是我自己。

3 个赞

先逐个回答一下:

跟人名有啥关系?这句意思很简单:即使是双胞胎身份证号也不一样。

所以鼓励使用复杂的名称注册指令,并让用户自行在 commands 中配置别称

  • 同一个插件只能有一个配置。多个同样的插件配置各自独立且拥有不同的依赖树
  • 事件与订阅者之间是不存在任何关联的,事件的广播是无状态的,无论订阅者有多少,0 到无限个,都与事件广播无关
  • 这个跑题了但还是回答一下:同一个指令当然可以有多个 action,只要符合“中间件”模型,但是你能保证插件市场 1000 多个插件的每一个 action 都是 return next() ?

再给一个场景 4:

在讲述场景前,提前告知的特性:

  • koishi 是异步加载,且插件加载不存在优先级
  • koishi 仅通过依赖关系确定加载顺序
  • ctx.command 本质上是在向一个指令中间件注册指令
    • 中间件的模型来自 Koa ,自行查找资料

有两个插件:

  1. 这是一个游戏查询插件,注册了名为 lc 的指令。
    描述:load card,用于加载用户卡牌
    功能:将用户卡牌从网站加载,并以 hash(username + cardname) 方式保存这些卡牌到 /data/cache
    指令返回:操作成功

  2. 这是一个清理插件,注册了名为 lc 的指令。
    描述:little cache,用于清理缓存
    功能:清理 /data/cache 目录的缓存文件
    指令返回:操作成功

用户经历了这些:

  1. 发现游戏查询插件有 bug:不会对 cache 目录下的过期卡牌文件进行清理,导致磁盘占用过多
  2. 发现有一个清理插件,可以清理 cache
  3. 使用清理 cache 指令 lc

此时应当怎么做?

  • 如何在相同输出结果下保证预期动作?
  • 如何找出它的 ID?
  • 靠什么保证了执行顺序不会是 缓存文件 → 清除缓存

如果你觉得这是场景举例本身有问题,那我觉得没有继续讨论下去的必要了,因为避重就轻和偷换概念不是一个好的讨论。


一言以蔽之:保证指令唯一性是架构效率、执行效率、开发效率与用户体验的平衡点。

当你想要重复指令时带来的后果是灾难性的:

  • 无法保证不同操作过程但相同返回的结果符合预期
  • 插件开发者在使用 session.execute 时有更大的编写负担:因为两个插件同一个指令返回是一样的,但执行带来的后果不一样

上述后果只是目前想到的冰山一角,重复指令我想不出任何优点。

只允许唯一指令的情况下,用户无需再记忆一个出现在指令周围的随机id,koishi 不需要为保证预期行为而额外增加指数级代码量,其他插件开发者不需要因为多个插件不同行为而产生更多的心智负担。

所以,我就明说了,偷懒使用各种简单至极且在不同类型下都非常容易撞车的常见指令名称的开发者就是懒狗,是傻逼,是一个及其不负责任的开发者。


如果你依然坚持,那么请:

  • fork koishijs/koishi 仓库
  • 在 internal/command 中加入你自己的逻辑
  • 自己维护这一份 fork

这是开源软件,欢迎另起炉灶。

8 个赞

所以说啦,回归原始,指令冲突了让俩开发者打一架就好辣()

4 个赞

这本身就是开发者间自己解决的事情,不需要也不应该浪费 koishi 产能在这种无意义的事情上。

4 个赞

Note: you can, and you should give your plugin command a relatively unique name, as the identifier for managing commands; in the meantime, you could also have aliases which easier to be recall to serve your functions. These two things did not conflict themselves.

4 个赞

我可以看出你是希望把指令唯一id和指令调用名拆成两个东西
然而即便如此也无法解决调用名的冲突问题,同时使用两个action函数是非常不实际的,两个指令大多数时候会有不同的参数列表
并且如此拆分损坏了目前提供的execute函数通过指令唯一id调用指令的功能,是严重的regression
我strong reject这个行为

5 个赞

很简单,因为Event Bus是触发者主导,而Command Router是监听者主导。

什么意思?

在Event Bus中,一个事件需要被触发,一定是对其定义了合适的、统一的类型的,而非随机匹配。在正常的软件设计中,你不会遇到事件名称是同一个,而它的事件参数完全不同的情况。你监听的事件是login,那么一定应当在触发时一个LoginEvent而不是Login2Event,如果你发现这种情况,那么很明显,你的代码有问题该修了。这是被写入类型的(至少在我接触的几个主流Event Bus实现中都是这样的),这些类型应该是在触发时就定义好的,因此这是"触发者主导"

但在指令系统中,不同的指令是完全有可能、也有机会拥有完全不同的参数列表的。因此你定义指令一定是在监听这个指令时定义,那么如果产生冲突,你觉得该怎么解决?

如果目标执行的指令并不是你需要的指令,怎么办?

这里举一个例子,我有两个指令,他们都叫execute,一个负责在本地执行一段bash代码(execute),一个负责在远程执行bash代码(execute.remote),如果我执行了execute rm /data/koishi.db,本来想清除本机的数据文件,结果却把另一台机子上的Koishi.db删除了,怎么办?反过来呢?

不一定所有的指令,都对于环境没有破坏性。你如何处理这种指令?

我想问:你这样做带来了什么好处?是用户的心智负担降低了,还是维护者的心智负担降低了?

首先,我们论证一下用户负担并没有降低: 如L所述,对于相同的指令,我们需要记住它的具体ID,这反而需要用户记忆更多,增加了用户的心智负担。

其次,我们论证一下维护者的心智负担没有降低:你说"一个指令可能被触发其中任意一个",那我请问:当你遇到"指令发生未知错误"时,你怎么知道哪一个插件出现了问题?你可能会说,显示一下现在被激活的指令不就行了?那我想说:Cordis(Koishi 核心)的插件加载顺序是不被保证的,你怎么保证这一次激活的指令,在下一次重启的时候还是激活的呢?

题外话,

这是完全不了解cordis,或者至少是Koishi开发理念的表现。Koishi开发有一个相当重要的性质是正交,即安装了插件B不允许侵入式改变插件A的行为。而这个特性很明显就是在侵入插件A的行为:如果两个插件都被安装,那么我们无法保证哪一个插件被正确执行。而cordis的特性决定了平行插件之间没有任何依赖关系。

4 个赞