我用 AI 逆向 Upwork 消息系统,2小时搞定数据层开发
前言
作为 Upwork 自由职业者,我一直觉得它的消息管理界面信息量太大,不够直观。我想做一个 Chrome 插件来简化消息管理,核心需求很简单:一眼看出哪些对话需要我回复,哪些在等对方。
传统做法是下载混淆后的 JS 文件慢慢分析,但这次我决定换个思路------全程和 AI 配合,看看能多快搞定。
结果远超预期。从零开始到完全摸清 API、认证方式、数据结构,总共不到 2 小时。
第一步:摸清技术栈(5分钟)
打开 Upwork 消息页面,F12 看 Sources 面板,从加载的 JS 文件名就能判断出技术栈:
bash
ThunderNuxt/rooms.fdb6ff58.js
ThunderNuxt/realtime.fa79131f.js
ThunderNuxt/composer.9c0ad3d8.js
"Thunder" 是 Upwork 消息系统的内部代号,基于 Nuxt.js(Vue 2 SSR 框架)。实时通信用了两条 WebSocket 连接,都基于 Atmosphere.js 框架:
ini
wss://tl.upwork.com/wp?app=thunder&... // 消息主通道
wss://tl.upwork.com/wp?app=global-dash-api&... // 通知/监控
同时还加载了 Forter、Incognia 两套反欺诈 SDK 和 OneTrust 隐私合规组件。整体架构很清晰。
关键发现:不需要下载和分析任何 JS 文件。 那些代码都是混淆压缩过的,变量名全是 o4Y、H1l 这种乱码。我需要的只是数据从哪来、长什么样。
第二步:找到 Vue 实例和 Store(15分钟)
Upwork 消息页面实际上有两个独立的 Vue 应用------顶部导航栏是一个微前端(spa_user.umd.js),消息主体是另一个。直接用 document.querySelector('#app').__vue__ 是找不到的。
最终通过遍历 DOM 定位到正确的入口:
javascript
void function() {
let s = null;
document.querySelectorAll('div').forEach(el => {
if (el.__vue__ && el.__vue__.$store && !s) {
s = el.__vue__.$store;
}
});
if (s) {
console.log('模块:', Object.keys(s._modules.root._children));
console.log('State:', Object.keys(s.state));
console.log('Actions:', Object.keys(s._actions));
}
}();
但发现了一个意外:Thunder 的 Vuex Store 里并没有消息数据模块。模块列表是 tracing、context、user、flags、theme 这些基础设施,消息数据完全走 REST API 获取。
这说明 Upwork 的消息内容不缓存在前端状态管理里,每次都是从服务端拉取。对我的插件来说反而更简单------直接调 API 就行。
第三步:抓取 API 端点(10分钟)
在 Network 面板筛选 Fetch/XHR,切换对话时可以看到所有请求。核心 API 一共就几个:
bash
GET /api/v3/rooms/rooms/simplified?limit=20&callerOrgId={orgId}
→ 对话列表
GET /api/v3/rooms/rooms/{roomId}/stories/simplified?limit=20&callerOrgId={orgId}
→ 消息列表
GET /api/v3/rooms/rooms/{roomId}/users?limit=100&callerOrgId={orgId}
→ 对话参与者
GET /api/v3/rooms/users/messageCounts?callerOrgId={orgId}
→ 未读数统计
所有请求都挂在 /api/v3/rooms/ 路径下,参数结构统一,非常规整。
第四步:搞定认证(5分钟)
直接调 API 会返回 401 Unauthorized。Token 在哪?
遍历 localStorage 立刻找到:
javascript
localStorage.getItem('f60cac5f103c5518_api_token')
// → "oauth2v2_int_36466ecdc9f06e6509a66b018cf9a60e"
加上 Authorization: Bearer 头就能正常请求:
ini
const token = localStorage.getItem('f60cac5f103c5518_api_token');
const headers = {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
};
const res = await fetch('/api/v3/rooms/rooms/simplified?limit=20&callerOrgId=' + orgId, { headers });
const data = await res.json();
对于 Chrome 插件来说,Content Script 运行在 Upwork 页面上下文中,用户已经登录,直接从 localStorage 读 Token 即可,不需要用户手动输入任何凭据。
第五步:解析数据格式(20分钟)
这是最有趣的部分。API 返回的不是常规 JSON,而是 Thrift 序列化的 JSON 格式,字段名全是数字编号:
json
{
"1": {"str": "room_f0ff6267bae1..."},
"2": {"str": "某某客户"},
"7": {"str": "项目标题"},
"8": {"map": ["str", "str", 26, {...}]},
"10": {"i32": 0},
"12": {"i64": 1771698648604},
"13": {"rec": {"1": {"str": "story_865b..."}, "8": {"str": "消息内容"}}}
}
看起来像加密?其实不是。Thrift 是 Apache 的跨语言序列化框架,数字编号只是字段 ID。通过对照页面上显示的内容和返回数据,每个字段的含义很快就反推出来了:
对话(Room)结构:
| 字段 | 类型 | 含义 |
|---|---|---|
| 1 | str | roomId |
| 2 | str | 对话标题 |
| 7 | str | 项目名称 |
| 8 | map | 上下文信息(合同ID、金额、状态等) |
| 10 | i32 | 未读消息数 |
| 12 | i64 | 最后更新时间戳 |
| 13 | rec | 最后一条消息 |
| 30 | str | 客户ID |
| 31 | str | 客户组织ID |
| 34 | str | 合同ID |
消息(Story)结构:
| 字段 | 类型 | 含义 |
|---|---|---|
| 1 | str | storyId |
| 2 | str | roomId |
| 3 | i64 | 创建时间 |
| 5 | str | 发送者ID |
| 8 | str | 消息正文 |
| 10 | str | 消息类型(系统消息) |
| 12 | lst | 关联对象(里程碑等) |
| 13 | tf | 是否已读 |
| 36 | str | 摘要文本 |
这些数字编号在 Upwork 内部的 .thrift 定义文件里有对应的字段名,但我们不需要那个文件,通过数据本身就能完全还原语义。
第六步:实现核心业务逻辑(5分钟)
有了数据结构,插件的核心逻辑就非常简单了。我最想要的功能是对话状态自动分类------判断"最后一条消息是谁发的"来决定状态:
css
function getRoomStatus(room, myId) {
const lastStory = room['13'];
if (!lastStory) return { label: '无消息', icon: '⚪' };
const senderId = lastStory['5']?.str;
const unread = lastStory['13']?.tf === 0;
const time = lastStory['3']?.i64;
const daysSince = (Date.now() - time) / 86400000;
if (senderId !== myId && unread) {
return { label: '新消息', icon: '🔴', priority: 1 };
}
if (senderId !== myId) {
if (daysSince > 3) return { label: '急需回复', icon: '🟠', priority: 2 };
return { label: '需要回复', icon: '🟡', priority: 3 };
}
if (senderId === myId) {
if (daysSince > 7) return { label: '对方可能忘了', icon: '💤', priority: 4 };
return { label: '等对方回复', icon: '🟢', priority: 5 };
}
}
五种状态,按优先级排序,一眼就知道该先处理哪个对话。
总结:AI 改变了前端逆向的游戏规则
整个过程没有下载任何 JS 文件,没有用反混淆工具,没有读一行压缩代码。
传统逆向流程:下载 JS → 格式化 → 反混淆 → 读代码 → 猜逻辑 → 试错。可能需要几天。
AI 辅助流程:告诉 AI 你看到什么 → AI 告诉你下一步查什么 → 把结果贴回来 → AI 分析含义。2 小时。
本质上 AI 充当了一个"有经验的逆向工程师搭档"。它知道 Nuxt 应用的 Vue 实例挂在哪里,知道 Atmosphere.js 是 WebSocket 框架,知道 Thrift 序列化长什么样,知道 OAuth Token 通常存在哪。这些经验以前需要人积累多年,现在一个对话窗口就搞定了。
前端 JS "加密"的门槛已经非常低了。只要数据要展示给用户,它就必须在浏览器里被解密和渲染,这个过程中的一切都是透明的。AI 只是让找到这些数据的过程变得极其高效。
插件完整代码将在下一篇文章中分享。
- What you came for.