4.9 KiB
UGC 文本过滤
服务端使用项目内 JSON 词库构建内存 Trie。命中部分按原文 Unicode 码点替换为 *,其余原文保持不变;消息不会因为命中词条而被拒绝。
编辑词库
文件:apps/api/src/config/text-filter.dictionary.json。
blockedWords:要屏蔽的词,增加或删除一项即可调整。allowedPhrases:允许包含屏蔽词的完整短语,用于修复误伤;只豁免完全位于短语内的命中,不放行整句话。- 每个词一行,使用双引号;JSON 不支持注释,最后一项后不要加逗号。
- 不允许空词、首尾空白、指定的零宽字符或星号;星号保留为打码字符。
- 英文大小写及全半角规范化后,同一列表不得重复,两类列表不得存在相同词条。
- 按字面匹配,不执行正则表达式。中文不分词,英文也按子串匹配;短词容易误伤,宜优先维护明确的完整表达。
以下仅为编辑格式示例,不属于正式词库:
{
"blockedWords": ["坏蛋"],
"allowedPhrases": ["坏蛋糕"]
}
上述配置会把“坏蛋糕,你是坏蛋”处理为“坏蛋糕,你是**”。
当前初始词库为 522 条,基于社区分类词库筛选,并保留原有 13 条辱骂表达及部分繁体变体。包含涉政人物、事件、表达与组织名,以及色情辱骂和部分违规招揽词。来源、许可及筛选边界见 初始词库整理说明。后续直接编辑 JSON,通过 Git 审阅并维护,不自动同步上游。
校验与生效
编辑后执行:
bun -e 'import { textFilter } from "./apps/api/src/lib/services/textFilter.ts"; console.log(textFilter.mask("这里填写需要检查的文本"));'
bun run test packages/shared/src/text-filter/index.test.ts
bun run build
第一条命令加载真实 JSON 并校验格式、重复项及冲突,同时预览结果。纯算法测试使用独立测试词库,不依赖正式词条;构建用于检查类型和打包,不能代替第一条命令的词库语义校验。
服务端直接导入 JSON,词库会包含在服务端 dist 产物中,不需要 Docker 额外复制文件。生产环境重新构建、部署并重启后生效;已运行进程不热加载。无效词库会在服务模块初始化时抛错,不静默跳过;空数组是允许的明确空词库。
抗干扰边界
匹配副本进行以下处理,并保留原文位置:
- ASCII 英文字母不区分大小写。
- 全角 ASCII 字符
U+FF01–U+FF5E转半角,表意空格U+3000转普通空格。不执行其他兼容字符或繁简转换。 - 屏蔽词匹配忽略
U+200B、U+200C、U+200D、U+2060、U+FEFF;未命中原文中的这些字符保持不变,包括正常 emoji 的连接字符。 - 在精确匹配之外,额外支持跳过普通空格、
.、·、_、-。相邻词字符之间最多一个此类分隔符,零宽字符不占该额度。 - 不跨越换行、制表符、逗号、中文句号等其他字符。含有分隔符的词条仍支持字面精确匹配。
- 例外词仅做大小写及全半角规范化后的连续匹配,不跳过分隔符或零宽字符。
- 不自动折叠重复字、不识别拼音或谐音;繁体和明确变体需要独立词条。
例如配置“骗子”后,“骗.子”“骗 子”都会变成三个星号,“骗---子”“骗,子”不会命中这条规则。命中时遮蔽整个原文跨度,包括内部干扰字符;多处及重叠命中取并集。星号数量按 Unicode 码点计算,不按 UTF-16 单元,也不按组合字素计算。
业务接入
import { textFilter } from '@server/lib/services/textFilter';
const { text, changed } = textFilter.mask(input);
业务先执行原有输入与长度校验,再过滤指定字段,保存、返回及广播同一份处理结果。不要把整个 JSON、ID、URL 或结构化物品数据交给过滤器。
当前接入 createCultivatorChatMessage:
- 世界与宗门聊天正文:
textContent和payload.text一致打码。 - 道具展示附言:在构建展示 payload 之前打码。
灵兽改名也已接入:POST /api/combat-v6/beasts/rename 校验角色归属、个体修订号和战斗占用,名字去除首尾空白后限 1~7 个 Unicode 码点,不允许内部空白、控制或不可见字符;过滤后保存到原有 name 字段,允许全星号和重名,同名提交不增加修订号。详情名字旁可打开改名弹窗;后续战斗使用新名字,历史回放不变。
角色昵称、公告、社区正文、道具快照中的名称等尚未接入,需要在各自写入入口接入。历史记录不自动重刷;不额外保存玩家原文,删掉屏蔽词也不会恢复已打码内容。
算法位于 packages/shared/src/text-filter,不访问数据库和网络;词库与单例位于服务端,前端不导入词库。以后需要动态维护时替换服务端加载方式,保留业务接口。