Files
2026-10-03 15:02:19 +08:00

4.9 KiB
Raw Permalink Blame History

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,不访问数据库和网络;词库与单例位于服务端,前端不导入词库。以后需要动态维护时替换服务端加载方式,保留业务接口。