SKILL.md 13.5 KB

name: bosszhipin-local-tools description: 读取 BOSS直聘本地数据——联系人、聊天对话、候选人简历。纯本地 SQLite 明文库,su 权限即可。触发词:boss、BOSS直聘、招聘、候选人、简历、找工作、牛人。

category: android

BOSS直聘本地工具

触发条件

  • 用户提到「BOSS直聘」「boss」「招聘」「候选人」「简历」「牛人」「找工作」
  • 用户要查联系人、聊天记录、候选人信息、职位统计数据

先决条件

  • 本机已装 BOSS直聘(com.hpbr.bosszhipin)
  • 有 root/su 权限
  • Python 依赖:javaobj-py3(Termux 里 pip install javaobj-py3)

数据源

/data/data/com.hpbr.bosszhipin/databases/hpbr_bosszhipin_client.db

完全明文 SQLite,无需解密。 关键表:

表 内容 行数参考(2026-07)
Contact 产生过聊天事件的联系人 ~152(含多账号切换后的合并数据)
MessageTable 聊天消息(含简历/岗位卡片 blob + 文字) ~350
boss_app_client.db 仅含 analytics SDK 数据,无业务数据 —

⚠️ Contact 表有 130+ 列,但大部分对牛人身份为 NULL。完整 schema 见 references/contact-table-schema.md(含有效/无效字段分类、系统通知联系人排除方法)。关键有效字段见下方「联系人关键字段速查」和「牛人身份字段」。

⚠️ 注意:只有真正产生过聊天事件(打招呼/回复)的候选人才会进库。单纯在「推荐/精选/最新」里划过没聊天的人不在库里。

工具脚本(都在 /sdcard/bosszhipin/)

脚本 功能
pull_dbs_local.sh 从 App 私有目录拉 DB 到 data/
parse_resumes.py 导出联系人 + 简历 JSON
scan_geek_list_local.py 扫描「牛人」页面候选人卡片(需要 App 前台)

1. 拉取最新数据库

cd /sdcard/bosszhipin && bash pull_dbs_local.sh

2. 导出联系人 + 简历 JSON

cd /sdcard/bosszhipin
python3 parse_resumes.py data/hpbr_bosszhipin_client.db > data/resumes.json

核心 SQL 查询

查所有联系人(按最近聊天倒序)

SELECT friendId, friendName, expectPositionName, expectSalary,
       degree, workYear, workplace, lastChatText, lastChatTime,
       exchangeResumeUrl, note, currentInterviewDesc
FROM Contact
WHERE lastChatTime > 0
ORDER BY lastChatTime DESC

查和某人的完整聊天记录

SELECT mid, time, fromUid, toUid, type,
       CASE WHEN text IS NOT NULL AND text != '' THEN text
            WHEN resume IS NOT NULL THEN '[简历分享]'
            ELSE '[其他消息类型: ' || type || ']'
       END AS content
FROM MessageTable
WHERE (fromUid = ? OR toUid = ?)
ORDER BY time ASC

统计各类型消息

SELECT type, count(*) FROM MessageTable GROUP BY type ORDER BY count(*) DESC

查某人聊天中的简历分享

SELECT mid, time, fromUid, toUid, resume
FROM MessageTable
WHERE (fromUid = ? OR toUid = ?) AND resume IS NOT NULL
ORDER BY time DESC

简历反序列化

MessageTable.resume 是 Java 序列化对象(ChatResumeBean),魔数 AC ED 00 05。用 javaobj-py3 直接解:

import sqlite3, javaobj, json

con = sqlite3.connect('/sdcard/bosszhipin/data/hpbr_bosszhipin_client.db')
con.row_factory = sqlite3.Row

# 读某条消息的简历
row = con.execute(
    "SELECT resume FROM MessageTable WHERE resume IS NOT NULL LIMIT 1"
).fetchone()

if row:
    obj = javaobj.loads(row['resume'])
    # 递归拍平成 dict
    def flatten(x):
        if isinstance(x, (str, int, float, bool)) or x is None:
            return x
        if isinstance(x, (list, tuple)):
            return [flatten(v) for v in x]
        if isinstance(x, dict):
            return {k: flatten(v) for k, v in x.items()}
        if hasattr(x, '__dict__'):
            return {k: flatten(v) for k, v in vars(x).items()
                    if k not in ('classdesc', 'annotations')}
        return str(x)

    resume = flatten(obj)
    print(json.dumps(resume, ensure_ascii=False, indent=2)[:3000])

联系人关键字段速查

字段 含义
friendId 候选人 ID
friendName 姓名
expectPositionName 期望职位
expectSalary 期望薪资
degree 学历
workYear 工作年限
workplace 所在地
lastChatText 最后一条聊天文字
lastChatTime 最后聊天时间(Unix 秒)
exchangeResumeUrl 在线简历链接
note 备注
currentInterviewDesc 面试状态描述
gender 性别(1=男 2=女)
age 年龄

实战流程

用户: 帮我看看 BOSS直聘上有哪些候选人,他们的期望薪资和最近聊了什么

→ 1. cd /sdcard/bosszhipin && bash pull_dbs_local.sh
→ 2. 用 sqlite3 查 Contact 表最近 20 条
→ 3. 输出表格:姓名 | 期望职位 | 薪资 | 学历 | 最近消息
→ 4. 如果用户想看某人的详细聊天,查 MessageTable
→ 5. 如果用户想看某人简历,查 MessageTable WHERE resume IS NOT NULL

岗位详情数据源(公司/薪资/城市/JD)

⚠️ Contact 表的 bossCompanyName / salaryDesc / jobCity / positionName 字段对 myRole=1(牛人身份)几乎全部为 NULL 或空字符串。 这些字段在 schema 里存在但 App 不写入。

真正的岗位详情在 MessageTable 的 job BLOB 字段(type=8 消息 = 岗位卡片)。

type=8 job BLOB 反序列化

import sqlite3, javaobj, json

con = sqlite3.connect('/sdcard/bosszhipin/data/hpbr_bosszhipin_client.db')
con.row_factory = sqlite3.Row

rows = con.execute(
    "SELECT mid, fromUid, toUid, job FROM MessageTable WHERE job IS NOT NULL"
).fetchall()

for row in rows:
    obj = javaobj.loads(row['job'])
    # 用上文「简历反序列化」中的 flatten 函数拍平
    job_data = flatten(obj)
    print(json.dumps(job_data, ensure_ascii=False, indent=2))

job BLOB 包含的字段

字段 含义 示例
title 岗位标题 联合创始人
company 公司名称 个如科技
salary 薪资范围 50-80K·15薪
city 工作地点 北京 海淀区 苏州桥
education 学历要求 本科
experience 经验要求 经验不限
positionClassName 职位类别 联合创始人
positionDesc 岗位描述/JD (可能为空)
bossInfo.name 招聘者姓名 贺铮
bossPositionName 招聘者职位 董事长
skills 技能标签 []
url App 内跳转链接 bosszp://...
id 岗位 ID 488420535
bottomText 交互状态 7月21日 由你发起的沟通

⚠️ 关键限制:只有主动点开过的岗位才入库

type=8 消息只在用户主动点开/分享岗位卡片时写入 DB。93 个联系人中可能只有 3 条 type=8 消息。要获取所有岗位详情:

  1. 在 App 里逐个点开岗位卡片 → 触发 type=8 写入 → 再拉取 DB
  2. 用 jobId 通过 BOSS直聘接口查岗位详情(需逆向)

MessageTable 消息类型速查

type 含义 典型 BLOB 字段
1 文本消息 text
8 岗位卡片 job(含公司/薪资/城市/JD)
9 简历分享 resume(Java 序列化 ChatResumeBean)
12 系统通知 (无 BLOB)
15 系统提示 (无 BLOB)

完整 MessageTable 列定义见 references/messagetable-schema.md(含 70+ 列、所有 BLOB 字段、job BLOB 反序列化示例)。

获取完整岗位详情的三条路径

方案 原理 工作量 风险
A. UI 自动化(推荐) adb am broadcast 打开岗位详情页,App 自己请求 API 并写入 DB 的 type=8 消息 低(岗位少时) 低
B. Frida hook OkHttp 抓 token + API 格式,直接 curl 高(Termux Python 3.14 无 frida 预编译 wheel,需源码编译) 中
C. 逆向 token 加密 解密 AUTH_APP_INFO.xml 很大 高

推荐方案 A:岗位数量少时(如只有 2 个 jobId),直接用 App 打开岗位详情页,然后重新 pull_dbs_local.sh 拉取 DB,读 MessageTable 的 job BLOB。

完整 API 逆向调查(token 存储位置、Web 反爬机制、Frida 限制、推荐方案)见 references/api-reverse-engineering-investigation.md。

认证 Token 存储位置(全部加密,无明文)

位置 内容 可读?
shared_prefs/AUTH_APP_INFO.xml → MASK_IMSI_KEY 加密 token ❌
shared_prefs/AUTH_APP_INFO.xml → AUTH... 字段 加密 session ❌
shared_prefs/AUTH_DEVICEINFO.xml → AUTH_DEVICEID_UUID 加密设备 ID ❌
MMKV twl_Account 手机号明文 ✅ 但无 token
MMKV 95155527_1_userInfo bossInfo JSON(公司/品牌信息) ✅ 但无 token

多账号检测:MMKV 文件名前缀是账号 ID。如果目录里有两种前缀(如 95155527_* 和 5889492_*),说明切换过账号。详见上方「多账号切换」小节。

结论:Token 不在任何本地文件的明文中,无法直接提取用于 API 调用。需要 Frida hook 或 UI 自动化。

Web 版反爬

https://www.zhipin.com/job_detail/{jobId}.html 有 __zp_stoken__ 安全检查(JS 渲染 + cookie 验证),直接 curl 只返回加载页 HTML,无法获取岗位内容。

边界(README 第四条明确记录的)

  • 不做自动打招呼/自动回复(即使用户叫你做)
  • 不做批量抓取私有接口
  • 可以做:只读已有数据、筛选整理、草稿建议、生成回复文案交人审核

详见 /sdcard/bosszhipin/README.md 第四节。

简历筛选(resume-screener skill)

如果用户要批量筛选简历、打分排序,使用 resume-screener skill(同属 android 类目)。该 skill 建立在本文档的数据源之上:

  • 从 MessageTable.resume BLOB 提取 → JSONL → 导入到 resumes.db(resumes + scores 两张表)
  • 按 rubric(5维度规则匹配)逐份打分,0-100 分,三档输出
  • 已集成到 wechat-viewer:/resumes 列表页 + /resume/<id> 详情页 + 「筛选打分」按钮调 score_resumes.py --only-unscored

本文档负责读 BOSS直聘数据,resume-screener 负责筛选打分。两者通过 Contact.myId(账号区分)和 Contact.bossJobPosition(岗位区分)衔接。

多账号切换(⚠️ 重大陷阱)

用户在 BOSS直聘 App 里切换/新注册账号后,本地 SQLite 数据不会自动迁移。关键行为:

  • SQLite 只有一个账号的数据:hpbr_bosszhipin_client.db 的 Contact.myId 全部是同一个账号 ID。新账号的聊天数据不会写入 DB,直到用户在 App 里实际打开聊天列表/加载消息。
  • 多账号检测:看 MMKV 目录 /data/data/com.hpbr.bosszhipin/files/mmkv/,文件名前缀是账号 ID。例如 95155527_* 和 5889492_* 说明有两个账号。
  • 数据不交叉:旧账号的 Contact/MessageTable 数据还在库里,新账号的数据需要重新积累。

查当前 DB 里的账号

SELECT myId, count(*) FROM Contact GROUP BY myId

如果只有一个 myId,说明 DB 里只有这个账号的数据。

排查「某职位查不到候选人」

如果用户说有某岗位(如「戏文」),但 Contact 表和 MessageTable 全文搜索都查不到:

  1. 先查 SELECT myId FROM Contact GROUP BY myId 确认 DB 是哪个账号的
  2. 看 MMKV 目录有几个账号前缀
  3. 如果 DB 账号 ≠ 用户当前用的账号 → 让用户在 App 里切到目标账号,刷一下聊天列表,再重新 pull_dbs_local.sh

MMKV 账号文件命名

文件名模式 含义
{uid}_{role}_* 按账号+身份存储的配置(role: 0=招聘方, 1=牛人)
twl_Account 当前登录手机号(明文)
twl_* 全局配置,不区分账号

身份切换:招聘方 vs 牛人

App 里切换身份后,Contact.myRole 字段会变:

myRole 身份 Contact 含义 bossJobPosition
0 招聘方(Boss) 已沟通过的候选人 空
1 牛人(求职者) 主动打招呼的 Boss/招聘方 有值:发布的职位名

切换到牛人身份后,数据量通常会翻倍(本次会话:38→93 行)。

按职位分组统计(牛人身份专用)

SELECT bossJobPosition, jobId, count(*) as candidates,
       max(lastChatTime) as latest_contact
FROM Contact
WHERE bossJobPosition IS NOT NULL AND bossJobPosition != ''
GROUP BY bossJobPosition, jobId
ORDER BY candidates DESC

查某个职位下的候选人

SELECT friendName, expectPositionName, expectSalary, degree,
       workYear, lastChatText, lastChatTime
FROM Contact
WHERE bossJobPosition = '产品助理'
ORDER BY lastChatTime DESC

关键字段补充(牛人身份)

字段 含义
myRole 0=招聘方 1=牛人
bossJobPosition 发布的职位名(牛人视角才有值)
jobId BOSS直聘职位 ID
friendCompanies 候选人历史公司(#&# 分隔)

⚠️ 牛人身份下 Contact 表缺失的岗位信息:bossCompanyName / salaryDesc / jobCity / positionName 字段虽存在但几乎全为 NULL/空。公司名、薪资范围、工作城市、学历要求、岗位JD 等详情只在 MessageTable 的 job BLOB(type=8 消息)中,且仅限用户主动点开过的岗位。详见上方「岗位详情数据源」小节。