聊天时右下角有一个可拖动的悬浮球,点开是快速面板:总开关、按会话单独开关、当前口癖清单。 按会话生效——在 A 聊天里关掉口癖,切到 B 聊天仍然是开的。齿轮打开完整设置页。
- 前缀 / 后缀,每条带权重,加权随机选一条
- 词语替换(默认
怎么→怎喵) - 作用范围:按句处理(每句单独决定加不加)或整条消息统一处理
- 屏蔽词:命中关键词的消息原样发出
- 跳过纯表情句(默认开);链接和密钥保护的是那一段,不是整句话
会话列表里图片和表情后面那句字是 QQ 自己写的(图片是「[图片]」,表情是「[表情]」,跟 QQ 语言
一致)。打开总开关后,插件把抽中的口癖塞进那对括号里([图片喵],不是 [图片]喵),
凑不齐成对括号就照原样接在后面。你自己写过的说明不会被覆盖。
每类一个下拉,四档选一 —— 不是一个开关能说清的:
- 关 —— 照 QQ 原样
- 跟主表一样 —— 用上面「口癖」段里那张主表
- 用公共口癖表 —— 从一张四类共用的表里抽
- 随机底句(整条消息) —— 底句换成你配的随机文案
随机底句是「整条」抽一次,不是每句各抽一次。一次会话里看的是同一条消息,逐句抽会抽出 自相矛盾的几句底句。底句表留空就不换,还是 QQ 那句,只按这一类的口癖接上去。
概率、名单、屏蔽词这些照常管用,跟正文消息走同一套。
抽签不是纯随机,否则同一条口癖会连着刷出来:
- 重复削减(
affixRepeatPenalty,默认 20%)——刚用过的下一条降权 - 饿死补涨——连续没被选中的按次数涨权:未达 5 次时每次 +25%,达到之后每次 +200%, 跳过越久的那条越容易把当前占优的压下去
所以「喵」和「的说」会交替着来,而不是连着三条「喵」。
设置页里每张口癖卡片除了权重还跟着一个 ≈33%:这一条在表里的基准占比。光看权重
看不出「100」是 1% 还是 50% —— 权重是相对值,除以全表总和才知道大概多大概率被抽中。
≈是有意的:这是按权重算出来的近似值,不是实时概率。上面那两条平衡规则会把实际分布 往均匀方向拉,所以低权重那几条显示值会明显低于实际(实测权重 25:75 实际是 30.9:69.1)。 它的用处是看出权重有没有填反、填成了 0,不是拿来当准数。
几层叠出来,从外到内:
- 总概率池 ——
probabilityExpr,支持+ - * / ( )和rand - 句长概率 —— 达到
maxLength的句子必定处理,更短的按lengthGradient缩放 - 保底命中 —— 按预期命中数算出一部分必定命中的位置:比例由
stability给,再加stabilityMinGuaranteed个保底;被选中的优先是长句 - 对仗强制 —— 打开
antithesisEnabled后,工整的对仗句必定加后缀,跳过上面几层 - 密集自动降概率 —— 打开
lengthAutoAdjustPool后,可操作位置够多时把总概率池降到 80%
发送路径和设置页的检测器走同一份计算,预览上标的百分比就是实际抽签用的那个数。
中英混排时口癖和正文之间要补空格,否则「好cat」这种粘连会被看出来。两个独立开关:
affixSpaceCjkLatin(默认开)——中文和拉丁字母之间affixSpaceLatinLatin(默认开)——拉丁字母之间
只补接缝:句子内部原有的空格不动,替换功能也不受影响。中英之间不加。
两个都默认关,装上就改别人消息不是这个插件该有的样子:
- 人机(
botChinesePeriod)——末尾没标点就补一个中文句号「。」,看起来像机器人在说话。 和抽签无关:这次没抽中口癖、甚至根本没配口癖,这一句照样加。已经自己敲了标点就不动, 末尾是链接或密钥也不动,免得把网址改坏 - 下一行算句尾(
lineEndsSentence)——多行消息里每个换行都算一句的结束,于是每行各补一个 句号。只管句尾补号,行内的逗号括号照旧切分、照旧各自抽一次口癖
一句话不会因为里面有个链接就被整句丢掉:
www.baidu.com is great.→www.baidu.com is great喵.,链接原样,后面那句照常加token-xxxxxxxxxxxx\nu can use this(回车分行)→ 密钥那行原样,后面那行加口癖- 只有当句子里除了受保护片段就什么都不剩(纯链接、纯密钥)时才整句跳过
- 口癖也不许贴在受保护片段上:句末正好是链接就不加后缀,句首正好是密钥就不加前缀
- 换行也算句子边界。少了这一条,上一行的密钥会把下一行正常的话一起拖下水
- 链接判定要求 scheme、
www.开头、字母顶级域或 IPv4,Python3.11、3.14、e.g.不会被误判
密钥和口令不加口癖(默认开):sk-、ghp_、AKIA、JWT、password= 这类已知前缀,
以及十六进制串、base64 片段、纯数字长串、带符号的口令。往密钥后面接一句「喵」,那条
密钥当场就废了。门槛是长度 >= 8,短的(vvvv、brrr)不拦;token is expensive 这种
普通英文也不会被误判(前缀表认的是 token=,带等号)。
- 只在聊天页出现,进后台自动收球
- 可拖动,位置和大小、透明度可调
- 面板里能按当前会话单独开关口癖
- 面板里只读列出当前口癖(类型、口癖本身、权重),点一下去设置页改
- 面板里改开关当场写盘 + 刷新缓存,立刻生效
一个 JSON 文件,放在 QQ 自己的 external files 目录:
/sdcard/Android/data/com.tencent.mobileqq/files/qqkuchiguse/config.json
- QQ 进程读写自家目录不需要任何权限,所以 hook 侧零权限,也不需要 root。
- 模块 App 写同一路径需要「所有文件访问权限」,保存失败会弹提示并引导授权。
同步是手动的,不做自动推送。 原因写在代码里:自动同步要么给每个 QQ 进程挂一个文件 监听(费电、还要兼容 QQ 的沙箱),要么加一个常驻 Binder 服务。实际做法是:
- QQ 进程启动时读一次进内存缓存,之后每条消息都用缓存,热路径上不碰磁盘;
- 在设置页改了配置,需要杀掉 QQ 重进才会生效;
- 或者在设置页底部点「重新读取配置」,当场重读盘。设置页跑在 QQ 进程里,刷新的是 同一份缓存。它会丢弃界面上没保存的改动,所以按钮上写清楚了。
想改代码的话,有几个地方是踩过坑才写成现在这样的,动之前先读代码里的注释:
- 会话归属靠一条栈,不靠快照。 AIO 数据对象是先构造、后填内容的,捕获那一刻
peerUid通常还是空的,所以 key 每次用的时候现读,不缓存。 - 「这一页结束了」用草稿 VM(
InputDraftVMDelegate)判断,不用ChatPie.onDestroy。 切换聊天时旧页的onDestroy也会触发,照它弹栈会把紧接着要用的会话提前弹掉。 - key 要在 AIO 对象上读,不能在
ChatPie上读——后者只是「哪个聊天页」的标记。 ChatPie的方法名全被混淆了,而且它不是View,连 View 树和 attach 回调都用不上; 认人只能靠它的构造器 hook。- 非主进程不装聊天 hook,只留
sendMsg。 - 重复处理要幂等。
sendMsg在主进程和 MSF 等子进程都有,口癖和补句号必须能安全地 跑第二遍,所以句首句尾已经有配置过的口癖时直接跳过。 - 界面里的预览必须和真正发出去的排版一致。 每一句自带自己的换行和空白,渲染时要先 拆成「前导空白 / 句身 / 尾部空白」再拼,不然「%」会显示到下一行、粉色也会定位不到。
- hook 安装在后台线程上做,不占 QQ 启动的主线程。
规则只有这一份实现,别再各自写一遍概率池。宿主模块被加载的每个进程里都能直接调:
import io.github.fnyoat.qqkuchiguse.hook.KuchiguseCore
// 只有会话 key(peerUid),拿不到就传 null
val out: String = KuchiguseCore.processText("原文", sessionKey)
// 有 QQ 的会话对象(sendMsg 第二个参数那个),让它自己解析
val out2: String = KuchiguseCore.processTextOfContact("原文", contact)
// 不知道当前在哪个会话时,可以问模块要
val key: String? = KuchiguseCore.currentSessionKey()约定:
- 不改就原样返回入参,返回值永远非空,调用方不用判空。判定「不改」的条件和发送 路径完全一致:模块没开、该会话被关掉、命中屏蔽词、或者概率池没抽中。
- 只返回字符串,不碰消息对象、不碰参数数组——要不要写回去由调用方决定。
- 不抛异常,反射或配置出错会吞掉并原样返回,不会从你的 hook 里炸出去。
- 任意线程可调。
currentSessionKey()不要缓存,切聊天就会变。 - 会记账:抽签会推进口癖的连用/饿死计数,所以别拿它当试算工具反复调用。想看效果用 设置页的检测器,那条路径不记账。
注意这是你自己 hook 的进程内调用:要在别的进程里用,那个进程也得加载本模块。
悬浮球不出现
先确认框架里模块已启用、作用域含 QQ、且 QQ 重启过。再看 logcat 里 KuchiguseSession 这个
tag:正常应该有 capture: ChatPie@... depth=1 这类日志。
气泡挂在会话列表上
会话跟踪需要「有一个聊天页开着」这个判据。如果你的 QQ 版本换了类名导致 hook 装不上,会出现
这种情况,日志里会有 class ... not found。
改了设置没反应 按上面「同步是手动的」那节处理:要么重启 QQ,要么在设置页点「重新读取配置」。
口癖连着出好几次
打开重复削减,或者调高 stability 让保底分布更均匀。检测器可以先看效果,它不记账。
MPL-2.0。每个源文件头部都有 Exhibit A 的声明。