Skip to content

Repository files navigation

弦外之音(JevGuide)

微信聊天里的关系进展助手:读屏 → 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 个,不依赖设备)

装到手机

  1. 装 Release 里下的那份 APK(不需要 root;它是 release 变体,不是 debug 变体)
  2. 打开 App → 「去开启无障碍」→ 系统设置里找到 弦外之音 → 开启
  3. 填密钥:
    • Jev 密钥(apikey_…,判读与攻略度用):console.typesafe.ai 登录 → API Keys → 新建
    • 聊天模型密钥(生成文案用):默认智谱 GLM(glm-5.3-flash),任何 OpenAI 兼容端点都行
    • 走视觉读屏的话,模型必须看得懂图(GPT-4o / Qwen-VL / Gemini 这类;纯文本模型不行)
  4. 两个都点一下「测试」;再点「测试视觉读屏」,看它能不能把你的聊天页念出来
  5. 微信里打开聊天 → 点浮条 / 磁贴「攻略一下」/ 通知栏「判读一下」→ 浮条上出现攻略度,点它看 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,一处可改可清(留空=用官方入口)。


配置速查

读取方式

项 默认 说明
读取方式 自动 自动=先无障碍树,读空转视觉 | 只用无障碍树 | 只用视觉读屏
视觉端点/密钥/模型 留空 留空就跟「聊天模型」共用一套;模型要能读图

常驻悬浮条

项 默认 说明
常驻显示攻略度浮条 开 关掉就回到"弹结果页"的老样子
判读完自动弹结果页 关 攻略度已经在浮条上,要看文案点浮条
系统无障碍快捷按钮 关 开着的话系统会在屏幕边上画个圆钮(和浮条功能重复),关掉它才消失

Jev 侧(判读)

项 默认 说明
接口密钥 空 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)

整套界面用的是 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 远程触发,不用手点通知栏
    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
    release 构建里没有这个 intent-filter(主清单里那个 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 条文案)、攻略度百分比、每联系人本地记忆、结果页

About

弦外之音 —— 微信聊天里的关系进展助手:读屏(无障碍树 / 截屏视觉)→ Jev 判读 + 攻略度 → 聊天模型出 3 条候选回复,攻略度常驻挂在屏幕上。不改微信、不发消息、不注入点击。

Topics

Resources

Stars

29 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages