global-censor的实现

开发背景

开发这个插件的契机是这样的。在一个大群里,内鬼问了chatGPT关于“如何赚钱”的问题时,机器人的回复触发了QQ的莫名其妙的不知道什么玩意的审查机制。1分钟不到的时间群就爆炸了,机器人被口球了一天。具体原因不明,大概是涉诈吧。

对于这样一个大群来说,即使新建群,损失的用户也是不可挽回的,群每炸一次,失去的部分群友就找不回来了。

所以,对于QQ或者说任何一个有审核机制的平台来说,都必须杜绝这种情况的发生,不论哪个插件回复的内容,都必须经过过滤!不然造成的损失可能是无法接受的。

可爱的梦梦和我说,“你可以写个10行插件,叫global-censor”,我想,是这个道理,然后就开始研究。

遇到的问题

要对所有发送的消息进行处理,那最好的办法就是在这些消息被发送之前,对他们进行处理。这样的话,只要监听before-send事件就可以了。

import { Context, Schema} from 'koishi'
import {} from '@koishijs/censor/lib'
export const using = ["censor"]
export const name = 'global-censor'
export interface Config {}
export const Config: Schema<Config> = Schema.object({})
export function apply(ctx: Context) {
  ctx.on("before-send",async (session)=>{
    /// DO SOMETHING
  },true)
}

我尝试了以下方案:

方案1

直接使用censor服务的transfer转换所有的消息元素——失败!

session.elements = await ctx.censor.transform(session.elements,session)

方案2

套一层上<censor></consor>然后让解析器自己去做处理——失败!

session.content = "<censor>"+session.content+"</censor>"

失败原因

失败的原因有两个,这里只说一个,剩下的会在其他困难那里描述的。
这是由于,此时的elements不一定就是text类型的,而如果是例如execute类型的话,现在转化是没有意义的。

例如:
<censor><execute>fuck</execute></consor>
当然这个例子比较夸张,不过也不是不可能吧()。就是fuck本身是可以execute的,而且不会返回任何敏感词,但现在这个词被过滤了,下一步就变成了<execute>****</execute>所以,实际上不会execute任何东西。
再例如:
<censor><execute>foo</execute></consor>
只过滤了文本foo而实际上foo指令执行出来的东西(例如“fuck”)不会被过滤,相当于过滤了个寂寞。

其他困难

暴力的text-censor

const filter = new mint_filter_1.default(words, { transform: 'capital' });

在作过滤之前,直接全变大写了,做完也不还原回去……导致实际上,会出现下面的情况。
<censor><execute>foo</execute></consor>过滤后变成<execute>FOO</execute>但实际FOO是不能execute的,不会返回任何结果。

总结

未过滤最后生成的文本

总的来看,监听这个事件时,最终的文本并未生成,不论如何过滤,接下来都会进行其他执行,导致过滤不尽如人意。

我认为解决这个问题有两个方案,

  1. 调整解析censor时的策略,让其后置,解析完censor内的所有消息元素再
  2. 添加一个新的事件befor-final-send,这个事件中的所有消息元素都不再解析,只当做字符串

未完善的censor生态

这个没什么好说的,如果上面的问题解决了,我去对着text-censor改改也整一个出来,也没什么很大问题。或者去text-censor提pr什么的,都不是关键问题。

3 个赞

说到这个,我觉得群成员冷库和浸泡插件也很有前途

1 个赞

细说()

2 个赞

就是说,群会爆炸是已知的事情
通常大家的做法是拉一个禁言的备份群,然后事后恢复,插件可以帮助这件事

3 个赞

已加入有生之年TODO

1 个赞

脱水!! 脱水!!!!

2 个赞

突然想起自己把这事鸽了,俺明天考完试就来看(下次一定

2 个赞

:laughing:
下次一定()

1 个赞

看了一下,感觉确实是设计的时候欠考虑了。

对于一个消息组件的渲染,有两种方案:

  1. 先对内层元素进行渲染,再调用外层元素的渲染函数
  2. 先调用外层元素的渲染函数,再遍历输出的元素进行渲染

目前统一采用的是方案 2,因为在许多场景下外层元素会直接把内层过滤掉,从而不需要渲染内层,节约大量性能。不过这个帖子说明了 1 也是有意义的。看起来需要额外提供相应的接口来实现。

2 个赞