AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例

我用 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 文件。 那些代码都是混淆压缩过的,变量名全是 o4YH1l 这种乱码。我需要的只是数据从哪来、长什么样。

第二步:找到 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 里并没有消息数据模块。模块列表是 tracingcontextuserflagstheme 这些基础设施,消息数据完全走 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.
相关推荐
JarvanMo1 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭2 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木2 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮2 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati2 小时前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉2 小时前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain
wuhen_n2 小时前
双端 Diff 算法详解
前端·javascript·vue.js
UrbanJazzerati2 小时前
Vue 3 纯小白快速入门指南
前端·面试
雮尘2 小时前
手把手带你玩转Android gRPC:一篇搞定原理、配置与客户端开发
android·前端·grpc