微信聊天里的关系进展助手:读屏 → Jev 判读 + 攻略度评分 → 聊天模型出 3 条候选回复,攻略度常驻挂在屏幕上。
介绍页:https://nisaka520.github.io/JevGuide/ | 下载:最新版 APK | 隐私政策:PRIVACY.md(网页版)
推广链接:设置页「去申请密钥」对部分厂商会带作者的邀请码 —— 不影响你的价格与权益, 你也完全可以自己去官网注册(详见 PRIVACY.md 第 11 条)。这是本项目目前唯一的收益来源。
跟同门两个东西的分工:
| 项目 | 形态 | 干什么 | 要 root? |
|---|---|---|---|
| JevIntent | FkWeChat 插件(Xposed) | 长按消息,弹 3 条判读提示 | 要 |
| JevBystander | 独立 App(无障碍) | 读屏判读,弹 3 条提示 | 不要 |
| 弦外之音(JevGuide,本项目) | 独立 App(无障碍 + 视觉) | 判读 + 常驻攻略度 + 3 条能直接发的回复 + 每联系人长期记忆 | 不要 |
三个都只做「读屏 + 调接口」,不改微信、不发消息、不注入点击。
| 仓库 | 是什么 |
|---|---|
Nisaka520/JevGuide(本仓库) |
弦外之音:源码 + 文档 + 单测,带完整开发历史(从 v1.0.0 起,几十次「真机实测 → 改 → 再测」的来回都在里面) |
Nisaka520/JevBystander |
同门:只读屏判读,不常驻、不生成回复 |
Nisaka520/JevIntent |
同门:FkWeChat 的 Xposed 插件(要 root) |
- 三个仓库都是独立项目,只共享同一套「读屏 + 调接口」的口径;弦外之音从 JevBystander 的读屏底座拆出来
- 密钥与 release 签名不在仓库里,历史里也没有(推之前逐提交扫过一遍):
keystore.properties与*.jks都被.gitignore挡掉;仓库里唯一的签名文件是keystore/debug.keystore—— 那是公开的调试签名,故意提交的 (本地与 CI 共用它,换一把就会报「应用未安装」) - 提交前跑一遍
python test/check_secrets.py
2026-09-23 在一加 Ace 3 Pro(Android 16 / 微信 8.0.76)上实测:
| 对象 | 带文字的节点数 |
|---|---|
微信聊天页 ChattingUI |
0 |
微信搜索页 FTSMainUI |
0 |
系统自带 uiautomator 抓微信 |
0(407 字节空树) |
| 系统设置 App | 15 |
| 桌面(本服务) | 280 行 |
系统自己的工具都读不到 —— 所以这不是本 App 的 bug:微信自己不给树,任何无障碍客户端 (包括 JevBystander、包括各种读屏软件)都读不到聊天内容。
好消息是截屏能拍到(不是 FLAG_SECURE),所以本项目多了第二条路:截图 → 视觉模型念成文字。
两条路可以在设置里切,默认「自动」=先试免费的无障碍树,读空了才花钱走视觉。
长按 / 磁贴 / 无障碍按钮 / 通知按钮 / 点浮条 / 自动触发
│
├─ ① 读屏(两条路,按设置自动选)
│ a11y:无障碍树 → 文字节点(免费、快;新版微信读空)
│ vision:截屏 → 视觉模型 → {标题, 谁说了什么}(约 5~10s,微信读不到时用)
│
├─ 取该联系人的本地记忆(摘要 + 关键事实 + 历次攻略度 + 近期对话)
│ └─ 作为「背景」拼进 state
│
├─ ② 调 Jev(一次请求 8 个问题)
│ 意图 / 情绪分布 / 着急 / 待回复 / 风险 / 建议动作 / 回复姿态 / 攻略度
│
├─ ③ 调聊天模型(OpenAI 兼容)
│ Jev 的结论 + 记忆 + 当前对话 → 3 条候选回复
│ 默认 ①稳妥(不犯错)②推进(往前一步)③有趣(轻松幽默)
│ 风格一共七套、最多同时选三套(撒娇 / 冷淡 / 正经 / 长辈 可在设置里换进来)
│
├─ 记忆回写:追加对话轮次 + 记下这次攻略度(算出与上次的差值)
│
└─ 出结果:**常驻浮条更新攻略度**(点它看 3 条文案),不再默认弹窗
两条外部依赖独立降级:Jev 挂了没判读,聊天模型挂了只剩判读 —— 任何一个挂掉都不会让整次分析白跑,记忆照常更新。
判读出来的攻略度会一直挂成一个小条浮在微信上面,而不是弹个窗看一眼就没了。
- 拖动挪位置(位置记进设置,下次还在那儿)
- 点一下打开结果页看 3 条文案;还没有结果时点一下就是判读一次
- 长按隐藏(设置页可再开)
- 服务重启后按上次的文字与分数原样恢复,不用等下一次判读
- 颜色跟结果页一致:≥70 绿 / ≥40 黄 / 其余红
实现上用的是 TYPE_ACCESSIBILITY_OVERLAY —— 无障碍服务可以自己加这个窗口,
不需要「显示在其他应用上层」那种要用户去系统设置翻的权限。代价是它只在本服务的生命周期内存在
(服务被系统杀掉,浮条一起消失 —— 这反而合理:没服务就没数据)。
实测踩过两次"怎么有两个",都是这么修的:
| 现象 | 真正原因 | 修法 |
|---|---|---|
结果页叠成好几层(dumpsys 里挂了 4 个 ResultActivity) |
结果页是对话框样式,singleTop 挡不住"服务用 NEW_TASK 拉起"这条路径的叠加 |
launchMode="singleInstance" + onNewIntent 里 recreate() 复用同一窗口;结果页打开期间用 ScoreOverlay.setSuppressed() 把浮条收起来 |
| 浮条旁边还有一个圆钮,点一下也弹"正在截图识别" | 那是系统给无障碍服务画的「无障碍快捷按钮」。配置里只要带 flagRequestAccessibilityButton,系统就会画,跟注不注册回调无关(dumpsys 里 requestA11yBtn=true) |
静态配置里去掉这个标志位,需要时由 syncButton() 用 setServiceInfo() 在运行时加 |
现在默认状态下,本 App 在屏幕上只有一个窗口(就是浮条)。
Jev 的 score 题型返回 [0, 档数-1] 的连续浮点。原本打算用 11 档(0%…100%)直接 ×10,实测被接口拒了:
HTTP 400 {"detail": "Too many score levels. Must have at most 10 levels."}
所以最终是 10 档 + 按 100/9 换算:
| 项 | 值 |
|---|---|
| 档位文字 | 0%、10%、…、90%(10 个) |
| 返回值 | 0~9 的浮点 |
| 百分比 | round(score × 100 / 9) |
精度靠浮点补足:6.48 → 72%;0 → 0%、9 → 100% 两端都能取到。
档位含义(写在提示词里,一旦上线别改字 —— 改字等于换量尺,历史评分不可比):
| 分档 | 含义 |
|---|---|
| 0% | 基本没戏(冷淡、长期不回、关系在恶化) |
| 30% | 能搭上话,但明显是我单方面热情 |
| 50% | 有来有往、聊天自然,但还没超出普通朋友的好感信号 |
| 70% | 有明确好感信号(主动找我、关心我、愿意单独见面) |
| 90~100% | 关系已很亲密,双方都清楚彼此心意 |
只评当前状态,不评潜力,也不因为一句客套话就往上抬;信息不足时给中间值。
同一段提示词打真实 Jev,三种关系下的评分:
| 场景 | 关系 | 最近对话 | 攻略度 |
|---|---|---|---|
| 冷场型 | 普通朋友 | 「在吗」→「嗯」→「最近在忙吗」→「还行」 | 48% |
| 热络型 | 情侣 | 对方主动问下班时间、要来接、约好去她念叨的店 | 87% |
| 中性型 | 同事 | 报表往来 | 51% |
3 条文案也用真模型跑过(gpt-5.6-luna / gpt-oss-120b),都稳定输出 3 段并用上了记忆:
【稳妥】
有空呀,陪你去看展挺好的~你想看哪个展?我提前安排一下时间。
---
【推进】
有空,走呀~正好最近也想和你出去逛逛。你把展的时间地点发我,我们一起规划一下。
---
【有趣】
有空啊,团子今天拆家你都需要出来散散心了😂 看展我陪你,顺便看看有没有什么能治愈你的小猫奴心情。
("团子"是记忆里的猫 —— 记忆注入确实生效了。)
视觉读屏也在真机上验过:把微信截图发给 gpt-5.6-luna,8.2 秒读回
标题=「文件传输助手」 + 我:明天有空吗?想请你吃个饭,一字不差、左右气泡判定也对。
- 存哪:
filesDir/memory/<key 的 SHA-1>.json,每联系人一个文件;另有index.json当目录(坏了自动回退扫目录)- 不用 SharedPreferences 一把梭:记忆会长大,塞进 SP 等于每次读写都搬整块数据
- 文件名用 SHA-1:微信标题可能带表情/斜杠/引号/换行,直接当文件名会非法或超长
- 记什么:滚动摘要(≤600 字)+ 关键事实(≤12 条)+ 历次攻略度(≤50 次)+ 近期对话(≤40 条)
- 摘要何时刷新:攒够「摘要刷新频率」条新对话后让聊天模型压一次(默认 8 条,可关)
- 内部有个水位线
turnsAtSummary:记着"这次摘要是基于多少条对话压出来的",否则同一批对话会被反复压缩、白烧调用
- 内部有个水位线
- 怎么用:作为「背景」拼进 Jev 的 state,同时也喂给聊天模型 —— 所以它记得"她养了只猫""下周三出差"
- 怎么清:设置页「查看记忆」看概况,「清空全部记忆」一键清;卸载 App 也一起没
不用自己编译:最新版 APK —— 推 tag 时 CI 会自动跑单测、打包并发布。 那份 APK 用的是仓库里那把公开的 debug 签名,跟本机 debug 版签名一致,所以能互相覆盖安装。
# 需要 JDK 17 + Android SDK(platform 35)
# local.properties 写 sdk.dir=...(或设 ANDROID_HOME)
./gradlew.bat assembleRelease # 对外分发的那个(产物 app/build/outputs/apk/release/app-release.apk)
./gradlew.bat assembleDebug # 调试包:包名带 .debug、可 adb 调试(产物 .../apk/debug/app-debug.apk)
./gradlew.bat testDebugUnitTest # 纯逻辑单测(140 个,不依赖设备)- 装 Release 里下的那份 APK(不需要 root;它是
release变体,不是debug变体) - 打开 App → 「去开启无障碍」→ 系统设置里找到 弦外之音 → 开启
- 填密钥:
- Jev 密钥(
apikey_…,判读与攻略度用):console.typesafe.ai 登录 → API Keys → 新建 - 聊天模型密钥(生成文案用):默认智谱 GLM(
glm-5.3-flash),任何 OpenAI 兼容端点都行 - 走视觉读屏的话,模型必须看得懂图(GPT-4o / Qwen-VL / Gemini 这类;纯文本模型不行)
- Jev 密钥(
- 两个都点一下「测试」;再点「测试视觉读屏」,看它能不能把你的聊天页念出来
- 微信里打开聊天 → 点浮条 / 磁贴「攻略一下」/ 通知栏「判读一下」→ 浮条上出现攻略度,点它看 3 条文案
国产 ROM 记得给 App 开自启动/后台白名单,否则无障碍服务会被省电策略杀掉(浮条也跟着没)。
真正的门槛不是密钥,是"地址填到哪一级、模型叫什么" —— 填了 https://api.deepseek.com(少个 /v1)就 404,
把 deepseek-chat 填到智谱的地址上照样报错。所以内置了国内 7 家预设:
选中一家 → 自动填好地址 + 聊天模型 + 视觉模型 → 你只需要去控制台粘密钥。
| 预设 | 地址 | 聊天模型 | 能看图 | 备注 |
|---|---|---|---|---|
| DeepSeek(官方,便宜) | https://api.deepseek.com/v1 |
deepseek-chat |
✗ | 生成文案最划算;视觉读屏要另配一家 |
| 智谱 GLM | https://open.bigmodel.cn/api/paas/v4 |
glm-5.3-flash |
✓ glm-5.3-flash |
默认走付费档(新号有赠送额度);想省钱可改回免费的 glm-4-flash / glm-4v-flash。注意是 /v4 不是 /v1 |
| 硅基流动 | https://api.siliconflow.cn/v1 |
Qwen/Qwen2.5-7B-Instruct |
✓ | 聚合开源模型,模型名要带 厂商/ |
| 阿里云百炼 | https://dashscope.aliyuncs.com/compatible-mode/v1 |
qwen-plus |
✓ qwen-vl-max |
必须走 compatible-mode 这个地址 |
| 月之暗面 Kimi | https://api.moonshot.cn/v1 |
moonshot-v1-8k |
✓ | 长上下文是强项 |
| 火山方舟 | https://ark.cn-beijing.volces.com/api/v3 |
你的 ep-… |
✓ | 模型名要先去控制台创建推理接入点 |
| MiniMax | https://api.minimax.chat/v1 |
abab6.5s-chat |
✓ MiniMax-VL-01 |
密钥是长 JWT,别只复制一半 |
| 自定义 | 自己填 | 自己填 | — | 任何 OpenAI 兼容端点 |
- 模型名会过时(厂商改名比 App 发版快),所以输入框始终可改:报错就去控制台复制当前的名字
- 不骗人:地址认不出是哪家时(自建/中转站),界面会明说"不属于任何内置预设",不会假装是某一家
(认域名时按域名边界匹配,
api.deepseek.com.evil.com不会被当成 DeepSeek) - 换厂商不会清掉你已经粘好的密钥(万一是同一家的第二个账号呢),界面只提示"记得换成这一家的"
部分厂商入口带作者的邀请码:你注册后作者会拿到少量额度奖励,你的价格与权益不受任何影响;
不想带码就直接去厂商官网注册,功能完全一样。所有链接集中在源码 Aff.kt,一处可改可清(留空=用官方入口)。
| 项 | 默认 | 说明 |
|---|---|---|
| 读取方式 | 自动 | 自动=先无障碍树,读空转视觉 | 只用无障碍树 | 只用视觉读屏 |
| 视觉端点/密钥/模型 | 留空 | 留空就跟「聊天模型」共用一套;模型要能读图 |
| 项 | 默认 | 说明 |
|---|---|---|
| 常驻显示攻略度浮条 | 开 | 关掉就回到"弹结果页"的老样子 |
| 判读完自动弹结果页 | 关 | 攻略度已经在浮条上,要看文案点浮条 |
| 系统无障碍快捷按钮 | 关 | 开着的话系统会在屏幕边上画个圆钮(和浮条功能重复),关掉它才消失 |
| 项 | 默认 | 说明 |
|---|---|---|
| 接口密钥 | 空 | apikey_…,约 100 字符 |
| 题目语言 | zh | zh / mix(英文问+中文选项)/ en |
| 模型 | jev-latest | 也可固定 jev-1.13.0 |
| 情绪显示条数 | 3 | 1 / 3 / 5 |
| 上下文句数 | 3 | 目标消息前 N 句一起给 Jev |
| 自动判读 | 关 | 检测到对方新消息就分析(防抖默认 1200ms) |
| 项 | 默认 | 说明 |
|---|---|---|
| 地址 | https://api.deepseek.com/v1 |
要填到 /v1 |
| 模型 | glm-5.3-flash |
任意 OpenAI 兼容模型名 |
| 生成候选文案 | 开 | 关掉只做判读,省一次调用 |
| 生成几条 | 3 | 1~5 |
| 项 | 默认 | 说明 |
|---|---|---|
| 攻略度评分 | 开 | 关掉就少问 Jev 一题 |
| 本地记忆 | 开 | 关掉则每次分析都当第一次聊 |
| 摘要刷新频率 | 8 条 | 关 / 5 / 8 / 15 / 30 |
| 结果显示方式 | 结果页 | 或「只弹提示」(同 JevBystander 的 3 条 Toast) |
整套界面用的是 Material 3(com.google.android.material:material),而且跟着系统走:
- 深色/浅色自动跟随系统(
Theme.Material3.DayNight) - Android 12 及以上会取壁纸的颜色(Material You / 动态取色)—— 所有颜色都取自主题属性,不是写死的色值,所以壁纸换色,界面跟着换
- 卡片分层用 surface-container 的色阶(不是阴影),这是 MD3 的做法
- 图标:描边聊天气泡 + 上升箭头,带单色层(Android 13+ 的"主题图标"会自己上色)
代价摆在明面上:为此引入了 Material + AppCompat,debug 包从 1.1 MB 涨到 6.0 MB。 release 也暂不混淆 —— Material 控件靠反射按类名 inflate,R8 裁错一个类, 只有真机点到那一屏才崩,单测看不出来。要瘦身请先在有真机的环境做一遍手工回归, 再打开
isMinifyEnabled/isShrinkResources。
浮条点开就是结果页:
- 顶部:攻略度 65%(↑ +8) 用 52sp 大字 + 进度条(≥70 绿、≥40 黄、其余红,进度条同色)
- 中间:Jev 的判读行(意图+情绪 / 着急 / 建议姿态)
- 下面:文案卡片,点一下复制(卡片带涟漪反馈);还有「复制全部」「重新生成」「关闭」
- 「重新生成」只重打聊天模型(Jev 的判读与攻略度不动),省一次 Jev 调用
- 结果页拉不起来时(后台启动被系统拦)自动退化成一条可展开的通知,结果不会丢
- 密钥、联系人表、日志:本机 SharedPreferences
- 每个联系人的记忆:本机
filesDir/memory/(本机文件,卸载即消失) - 没有云端、没有统计、没有任何联网的第三方 SDK;崩溃上报、埋点一概没有
- 界面上引了 Google 的 Material Components 与 AndroidX(Apache-2.0):只在本机画界面, 不采集也不上传。所以现在不再是"零第三方依赖"了 —— 这里如实说明; HTTP 依旧是自己写的,没引 OkHttp/Gson
- 视觉读屏会把截图发给你自己配的那个端点(跟文案同一个模型)—— 这是这条路的代价,介意就切回「只用无障碍树」
- 截图只在内存里转成 base64 发走,不落盘
- 不修改、不发送任何消息,也不注入点击
- 新版微信读不到无障碍树(见开头实测表)→ 只能走视觉读屏,每次多一次带图调用(约 5~10s)
- 视觉读屏依赖"模型看得懂中文聊天截图":字号特别小、气泡被折叠、图片消息里的字都可能读错
- 视觉读屏只读屏幕上可见的那几屏:往上翻的历史看不到(和无障碍路线一样)
- 必须精确匹配包名
com.tencent.mm:微信分身/克隆版读不到 - 攻略度是模型判断,不是测量:同一段对话换一次调用可能差 ±10%,看趋势比看单次数值有意义
- 文案是模型草稿,发出去前自己过一眼
app/src/main/java/io/github/nisaka520/jevguide/
WatchService.kt 无障碍服务(事件监听、防抖、常驻通知、诊断抓屏、浮条生命周期)
WeChatReader.kt 无障碍树 → 屏幕消息(只用 text/contentDescription + 坐标)
VisionReader.kt 截屏 → 视觉模型 → 屏幕消息(微信不给树时的替代路线)
Digest.kt 抓屏结果 + 拼 state + 正文识别规则(纯函数,可单测)
DigestBuilder.kt 标题识别、引用块合并
Contacts.kt 联系人别名表 → 关系/性别/备注
Config.kt 全部设置(SharedPreferences)
SettingsActivity.kt 唯一的设置界面
Prompt.kt Jev 的 8 个问题 + 攻略度档位与换算(口径冻结)
JevHttp.kt Jev 客户端(HttpURLConnection,零依赖)
Verdict.kt 响应解析 + 排版(纯逻辑,可单测)
Analyzer.kt 编排:读屏 → 记忆 → Jev → 聊天模型 → 回写 → 浮条/结果页
Memory.kt 每联系人记忆(模型 + JSON + 文件持久化 + 上下文块)
MemoryUpdater.kt 摘要压缩(调聊天模型,异步、失败无害)
ChatHttp.kt OpenAI 兼容客户端(纯逻辑解析,可单测)
ReplyPrompt.kt 3 条文案的提示词 + 宽容解析(含七套风格、标题与条数的唯一来源)
StylePick.kt 风格点选器的判定(纯函数:上限/下限,拒绝时一点状态都不动)
Emphasis.kt 把说明文字里的 **强调** 解析成加粗区间(纯函数)
ScoreOverlay.kt 常驻悬浮条(无障碍浮层;format/colorOf 是纯逻辑,可单测)
ResultActivity.kt 结果页(攻略度 + 判读 + 可复制的文案卡片)
Toast3.kt 3 条 Toast 的排版与节流
Json.kt 手写 JSON(零依赖)
AppLog.kt 本机日志(同时写 logcat,TAG=JevGuide,方便 adb 远程看结果)
TileTrigger.kt 快捷设置磁贴
AnalyzeReceiver.kt 通知按钮落地
- 运行期第三方依赖只有 Google 的 Material Components + AndroidX(画界面用,Apache-2.0,不联网也不采集);HTTP 与 JSON 仍然自己写,没引 OkHttp/Gson。无障碍服务是常驻进程,少带一个库就少一份自己控制不了的东西
- debug 版专属调试入口(
app/src/debug/AndroidManifest.xml):把判读广播开放出去,方便 adb 远程触发,不用手点通知栏release 构建里没有这个 intent-filter(主清单里那个adb shell am broadcast -a io.github.nisaka520.jevguide.NOW -n <包名>/io.github.nisaka520.jevguide.AnalyzeReceiver adb shell am broadcast -a io.github.nisaka520.jevguide.DUMP -n <包名>/io.github.nisaka520.jevguide.AnalyzeReceiver
exported="false") - 想拿真模型验提示词:
PromptDumpTest会把拼好的 system/user 落到app/build/prompt-dump.txt - 提交前跑
python test/check_secrets.py,别把密钥带进仓库
- v1.3.0:改名「弦外之音」+ 换图标(蓝气泡里列三个选项);提示词重写成「像真人发微信」; 回复风格七套里自选三套(点选块、点一下实时生效、上限拦三层);补发布签名。 改动细节与真机验证结论见 CHANGELOG
- v1.2.0:整套界面换 Material 3(含深色模式与跟随壁纸取色),浮条改胶囊形
- v1.1.0:加视觉读屏(截图 → 视觉模型)与常驻悬浮条;修明文 HTTP 被拦
- v1.0.0(首版):从 JevBystander 拆出独立 App,加聊天模型(3 条文案)、攻略度百分比、每联系人本地记忆、结果页