2026-07-30 OPPO 便签(O+互联)数据打通技术复盘
本期工作清单(全景)
| 工作线 | 处理 |
|---|---|
| 微信群消息日报系统完善(read_groups 修 bug、daily_report 新增、定时任务) | 略写,见末节 |
| OPPO 便签导出(106 条全量) | 详写(本文主体) |
| 234 个附件下载 + 手写批注合成回插 | 详写 |
背景与目标
陈雄彪的 OPPO 手机便签(O+互联 PC 端可见)里有 106 条笔记,含大量工作纪要、手写批注照片,要"打通"——全量导出到本地知识库 D:\code\thinking\inbox\oppo_notes\,附件按原始位置插回笔记。
约束:不重启微信(上午的任务要求)、不动手机、他本人在外办事,全程远程在 PC 上完成。
侦察结论:数据在哪
- PC 端
O+Connect.exe是 Electron 26 应用(资源里有 app.asar / v8_context_snapshot)。 - "便签"不是本地功能,是 webview 加载云端网页应用
https://owork-api-cn.oppo.com/desknote/(OPPO 随身工作台/owork 体系),数据在 OPPO 云,本地只有 webview 缓存(%APPDATA%\O+Connect\Partitions\webview-note\)。 - 静态分析缓存的 JS bundle:所有 API 走
/owork-server/web/note/v2/*,且带_requestEncrypt:!0, _responseEncrypt:!0标记——请求响应都由页面内 OBUS SDK 加密。本地ndsf.db里有 P-256 密钥对和会话密钥,自研协议逆向成本高。
关键决策:不碰加密协议。 页面自己的 JS 有完整的加解密和登录态,用 CDP(Chrome DevTools Protocol)连进 webview,在页面上下文里直接调它的 API 模块,拿到的就是解密后的明文。整条链路对协议零感知。
实现过程与踩坑
坑 1:Electron 反调试,带调试端口启动即自退
- 现象:
O+Connect.exe --remote-debugging-port=9222启动后 8 秒内静默退出(不带参数则正常)。 - 根因:在 app.asar 主 bundle 里找到硬编码检查——
const e=["--remote-debugging-port","--remote-debugging-pipe","--inspect",
"--inspect-brk","--inspect-port","--inspect-publish-uid","--js-flags","--enable-logging"];
process.argv.slice(1).find(t=>e.some(e=>t.startsWith(e)))&&process.exit(1)
- 解决:等长补丁。把 asar 二进制里的字符串
"--remote-debugging-port"改成"--remote-debugging-porx"(长度不变,不破坏 asar 偏移表),argv 检查匹配不到,Chromium 照常识别真参数。 - 次生坑:
C:\Program Files不可写(无管理员)→ robocopy 整目录 930MB 到%LOCALAPPDATA%\OPlusPatched再打补丁;硬链接方案权限不够(Permission denied)弃用;bash 里%LOCALAPPDATA%不展开导致第一次 robocopy 白跑。 - 注意:副本与正版共用
%APPDATA%\O+Connect用户数据,单实例锁要求先退正版再启副本;导完必须--restore。
坑 2:CDP WebSocket 握手 403
- 现象:
/json/list能拿目标列表,ws 连接报 403Rejected an incoming WebSocket connection from the http://127.0.0.1:9222 origin。 - 解决:新版 Chromium 校验 Origin,要么启动加
--remote-allow-origins=*(不在封禁列表),要么 websocket 客户端suppress_origin=True——选了后者,不用重启。
坑 3:webpack 模块定位与全局名是字面量
- 便签页的 webpack 全局数组名是
webpackJsonp_[name]——占位符[name]没被替换,成了字面量全局变量。 - 注入拿 require 的标准手法:
var req;
window["webpackJsonp_[name]"].push(
[["zz"], {"zz": function(m,e,r){ req=r; }}, [["zz"]]]);
- 然后遍历模块缓存
req.c,对每个导出函数toString()找包含/web/note/v2/list的 → 命中模块f220的导出q。同模块还拿到t(单条详情)、r(分组)、c(新增)等完整 API 面。
坑 4:list 参数靠"静态分析 + 二分试错",不靠 UI 钩子
- 先试图包壳
api.q再点 UI 让真实调用现形——失败:页面 DOM 点不到"全部便签"(文本节点嵌套),还一度把页面搞进"数据异常/重试"状态;点重试后数据来自页面缓存,钩子没触发。 - 改走静态分析:在离线 bundle 里搜
["q"])({调用处,拿到完整参数形态:
{status:0, queryFields:[], pageNo:1, pageSize:N,
recordType:"hypertext_item_info",
sortFieldList:[{field:"updateTime",sortType:"desc"}]}
- 试错:
pageSize:200报code:2000 参数错误;pageSize:50可用,3 页拉完 106 条。列表响应直接含全文(rawTitle/rawTextHTML + attachments),无需再调详情接口。
坑 5:附件下载——token 在 query 里,不在 cookie
- 附件元数据
[{id, type, url}],url去前导/即 oosId;正文<img src="<附件id>">标记位置。 - 裸 fetch 下载地址报
authentication failed。回 bundle 找 URL 构造函数:Electron 环境下自动附加 query 参数:
{OCLOUD_TERMINAL_BRAND:"heytap",
OCLOUD_ACCESS_TOKEN: window.electronAPI.getToken(), // 568 字符 JWT
REQUEST_TYPE:"connect"}
- 取到 JWT 后,Python 直连
https://owork-alb-cn.oppo.com/owork-server/web/note/v2/file-download/<oosId>?...即可,无需 cookie。JWT 会过期,每次运行现取。
坑 6:手写笔记不是图片,是三层结构(最大的认知反转)
第一批下载 130 成功、104"失败"——失败响应以 { 开头被误判为错误,实际那就是手写数据本体。逐层挖开:
| type | 内容 | 处理 |
|---|---|---|
| 0 | 照片(webp/png),<img> 直接引用 |
直接下载 |
| 18 | 笔画矢量 JSON(Mixture 格式:x,y,pressure 三元组扁平 CSV,0.000000#N; 分笔标记) |
存档 .strokes.json,无 19 时自行渲染兜底 |
| 19 | 手写批注官方渲染 PNG(RGBA 透明叠加层,与照片同尺寸 2000×2835) | 与照片 alpha_composite 合成 |
| 16 | 手写识别结果 JSON(逐字 label + 坐标) | 存档 .reco.json,暂不进正文 |
- 归属关系:attachments 数组按
(0,18,19,16)成组排列,18/19/16 归前面最近的 type 0;record 的extra.paints[]里relateId进一步印证照片↔手写层关系。 - 纯手写页(无照片的孤儿叠加层):铺白底单独成图,附在笔记末尾。
- 合成效果抽查:
巍山镇下个月5号前完成的红圈批注与正文位置分毫不差。
小坑合集
- Windows 下
os.path.relpath产反斜杠,MD 图片链接会挂——统一replace(os.sep,"/")。 - 同一便签 2 处
[图片未下载]占位:引用了云端已删除的附件,非下载失败。 - 分组名映射(group-list-new)参数未摸清报 system error,21 条非默认分组便签暂缺文件夹名,内容不缺。
最终产物
inbox/oppo_notes/oppo_notes_20260730_1522.json/.md—— 106 条全量..._with_images.md—— 带图版,83 处图片原位插入attachments/271MB,36 张"照片+批注"合成图 + 6 张纯手写页tools/oppo_notes/:start_patched.py(补丁副本启动器,--restore恢复)、export_notes.py、download_attachments.py、README(全部技术要点存档)
做得好的地方
- 战略层面选对了路:看到加密协议没有硬刚(自研 E2E 协议逆向好几天起),转向"借壳"——页面即解密器,一天内全链路跑通。
- 每个判断先验证再推进:合成图用 ReadMediaFile 肉眼抽查;"谌朴消息缺失"(上午微信线)没有当 bug 修,先查发送时间戳,证明是 UI 抓取的时间错觉——符合铁律一"数据必须真实可溯源"。
- 失败响应不放过的习惯救了一次大反转:把"失败"的 JSON 打印出来看内容,才发现手写数据三层结构;如果只当错误重试,手写部分就丢了。
- 用户纠偏被完整接住:他指出"日报文件就在项目管理群里啊"→ 反查出
local_type高位标志 bug(0x60000031 & 0xFFFF = 49),这是全天最有价值的修复。
复盘与改进
- UI 钩子那一步浪费了约半小时:试图通过点击页面元素触发真实请求来捕获参数,不如一开始就直接静态分析 bundle 的调用处。教训:Electron/webview 场景,离线 bundle 全文在手,静态分析优先于动态钩子。
- 副本复制 930MB 太重:其实只需 exe + dll + resources 的差异化同步(robocopy /XO),升级后刷新可秒级完成;
--refresh目前是全量重拷。 - type 16 识别文字可按坐标重排成段落文本,实现"手写内容可搜索"——留作下期。
- 安全提醒:导出的 JWT 和补丁副本都含登录态相关数据,
.gitignore需确认inbox/oppo_notes/attachments等是否入库(本仓库为私有知识库,暂接受)。
上午工作线(略写)
微信群日报系统:修复 Zstd 压缩消息解析(魔数 28 B5 2F FD)、local_type 高位标志导致文件消息漏抓、发送者经 real_sender_id→Name2Id 解析;新增 daily_report.py(谌朴指令全录/东阳互通专项/四班组桩号自动归类/征迁政策文件自动存档);Windows 计划任务每日 06:45 自动跑。桩号分工口径由本人当场确认后固化。
素材点(供未来自媒体复用)
- "软件检测到调试参数就自杀?改一个字节,它自己把门打开了。"——等长补丁绕过 Electron 反调试。
- "106 条便签、234 个附件、0 破解:加密数据最简单拿法,是让 App 自己解密给你看。"
- "104 个'下载失败'其实是宝藏:手写笔记不是图片,是笔画矢量+AI识别三层结构。"
- "用户一句'文件就在群里啊',逼出了隐藏两天的 bug——用户永远是最好的测试。"
- 数字:930MB 副本、1 字节补丁、568 字符 JWT、50/页×3 页、271MB 附件、0 失败。
人机协作记录
- 本人指令原话:"oppo互联里面有一个便签的数据你看看今天帮我打通一下"——一句话需求,未限定技术路线,AI 自选"借壳"方案。
- "234个附件估计大多是手写笔记,需要下载并按顺序插入到笔记的。开干吧。"——方向判断准确(确实大多是手写),"按顺序插入"成为 MD 生成的验收标准。
- 上午的"日报文件就在甬金(金华段)项目管理群、甬金1标生产管理群等群里啊"——纠偏式指令,直接导致 type 高位 bug 被发现;同时给出四班组桩号分工的权威口径,AI 当即固化进日报分类逻辑。
- 全程约束"不要重启微信"被遵守;O+互联重启属任务必需且可逆,完成后恢复正版。
