qqkuchiguse

目前最强大,针对性最强的QQ消息自动处理Xposed。 给发出的 QQ 消息自动追加口癖。 前缀、后缀、词语替换,带概率、平衡和分句处理。

License: MPL-2.0 Xposed API 82+ Android 7.0+ Kotlin


聊天时右下角有一个可拖动的悬浮球,点开是快速面板:总开关、按会话单独开关、当前口癖清单。 按会话生效——在 A 聊天里关掉口癖,切到 B 聊天仍然是开的。齿轮打开完整设置页。

功能

改写消息

  • 前缀 / 后缀,每条带权重,加权随机选一条
  • 词语替换(默认 怎么 → 怎喵)
  • 作用范围:按句处理(每句单独决定加不加)或整条消息统一处理
  • 屏蔽词:命中关键词的消息原样发出
  • 跳过纯表情句(默认开);链接和密钥保护的是那一段,不是整句话

图片和表情的外显

会话列表里图片和表情后面那句字是 QQ 自己写的(图片是「[图片]」,表情是「[表情]」,跟 QQ 语言 一致)。打开总开关后,插件把抽中的口癖塞进那对括号里([图片喵],不是 [图片]喵), 凑不齐成对括号就照原样接在后面。你自己写过的说明不会被覆盖。

每类一个下拉,四档选一 —— 不是一个开关能说清的:

  • 关 —— 照 QQ 原样
  • 跟主表一样 —— 用上面「口癖」段里那张主表
  • 用公共口癖表 —— 从一张四类共用的表里抽
  • 随机底句(整条消息) —— 底句换成你配的随机文案

随机底句是「整条」抽一次,不是每句各抽一次。一次会话里看的是同一条消息,逐句抽会抽出 自相矛盾的几句底句。底句表留空就不换,还是 QQ 那句,只按这一类的口癖接上去。

概率、名单、屏蔽词这些照常管用,跟正文消息走同一套。

口癖平衡

抽签不是纯随机,否则同一条口癖会连着刷出来:

  • 重复削减(affixRepeatPenalty,默认 20%)——刚用过的下一条降权
  • 饿死补涨——连续没被选中的按次数涨权:未达 5 次时每次 +25%,达到之后每次 +200%, 跳过越久的那条越容易把当前占优的压下去

所以「喵」和「的说」会交替着来,而不是连着三条「喵」。

设置页里每张口癖卡片除了权重还跟着一个 ≈33%:这一条在表里的基准占比。光看权重 看不出「100」是 1% 还是 50% —— 权重是相对值,除以全表总和才知道大概多大概率被抽中。

≈ 是有意的:这是按权重算出来的近似值,不是实时概率。上面那两条平衡规则会把实际分布 往均匀方向拉,所以低权重那几条显示值会明显低于实际(实测权重 25:75 实际是 30.9:69.1)。 它的用处是看出权重有没有填反、填成了 0,不是拿来当准数。

命中概率

几层叠出来,从外到内:

  1. 总概率池 —— probabilityExpr,支持 + - * / ( ) 和 rand
  2. 句长概率 —— 达到 maxLength 的句子必定处理,更短的按 lengthGradient 缩放
  3. 保底命中 —— 按预期命中数算出一部分必定命中的位置:比例由 stability 给,再加 stabilityMinGuaranteed 个保底;被选中的优先是长句
  4. 对仗强制 —— 打开 antithesisEnabled 后,工整的对仗句必定加后缀,跳过上面几层
  5. 密集自动降概率 —— 打开 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 启动的主线程。

给其它 Xposed 模块用的 API

规则只有这一份实现,别再各自写一遍概率池。宿主模块被加载的每个进程里都能直接调:

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 的声明。

Releases