逆向京麦客户端提取京东聊天表情映射表


本文全部内容由Opus 4.7 技术总结生成

最近在做电商客服质检的项目,对接京东平台拉取客服和顾客的聊天记录,准备喂给 LLM 分析意图、打标签。跑起来看样本数据,发现消息文本里经常混着 #E-s38#E-b07#E-a10 这种奇怪的字符串。

一开始以为是乱码或者没处理干净的转义,仔细看才意识到这是京东自有的表情符号占位。最头疼的是,翻遍京东开放平台文档,压根没给映射表。

表情占位符虽然是小问题,但是还是很值得处理下。

前端展示聊天记录时,客服和顾客都很常用表情,满屏 #E-xxx 字符非常影响阅读,显得不专业,重要的是不够帅。

找映射表的几条路

1. 开放平台文档

先翻官方文档,从 京东服务平台 到咚咚 IM 的 API 文档基本翻了一遍,没有表情映射相关内容。消息结构里只有 content 字段,表情以占位符形式直接存在于文本中,文档也没说怎么渲染。

放弃,再提工单,回复”需要先和咚咚产品确认表情数据是否能透出”。这种肯定是遥遥无期的。

2. 关键线索:客户端自己渲染

试着把 #E-s38 这个字符串直接复制到京麦客户端(咚咚)的聊天输入框里,回车发送 —— 客户端直接把它渲染成了一个表情图片。

这说明映射关系就藏在客户端本地。不需要猜,不需要抓包,直接扒客户端资源就行。

扒客户端资源

京麦 PC 端有 Windows 和 Mac 两个版本。我用的 Mac,先在 /Applications 下找:

1
2
ls /Applications/ | grep -i -E "jing|京麦|dongdong|咚咚"
# 京麦.app

然后看 .app 的结构:

1
2
ls "/Applications/京麦.app/Contents/"
# _CodeSignature CodeResources config Frameworks Info.plist Library MacOS PkgInfo Plugins Resources translations

FrameworksPluginstranslations 这些目录,还有 .qm 文件,一看就是 Qt 应用,不是 Electron。Electron 最好扒(app.asar 解压就出源码),Qt 应用就得看资源文件散在哪。

定位表情目录

直接全局搜 emoji/face/smile 这些关键词:

1
2
3
find "/Applications/京麦.app/" -type d -iname "*emo*"
find "/Applications/京麦.app/" -type d -iname "*face*"
find "/Applications/京麦.app/" -type d -iname "*smile*"

命中:

1
2
3
/Applications/京麦.app/Contents/Frameworks/咚咚工作台.app/Contents/Resources/face
/Applications/京麦.app/Contents/Frameworks/咚咚工作台.app/Contents/Resources/face/face2x
/Applications/京麦.app/Contents/Frameworks/咚咚工作台.app/Contents/Resources/face/face3x

注意这里有个套娃:京麦 app 里嵌了一个”咚咚工作台” app,表情资源在子 app 里面。face2xface3x 对应普通屏和 Retina 屏的高清图。

看看里面有啥:

1
2
3
4
ls "/Applications/京麦.app/Contents/Frameworks/咚咚工作台.app/Contents/Resources/face/face2x/"
# b01.png b02.png ... b26.png
# j01.png j02.png ... j20.png
# s01.png s02.png ... s94.png

一共 112 个 PNG,分 s/b/j 三组。图片是有了,但是关键的英文代码到中文含义的映射还在别的地方。

找映射关系

接着在咚咚工作台的 Resources 下翻:

1
2
3
4
5
6
7
Resources/
├── css
├── face
├── image
├── jsfile ← 这个
├── language
└── ...

jsfile/ 下面有几个子目录:chathtmlreceptionHtmlmonitorjs 等。既然客户端是用 HTML + JS 渲染聊天区(Qt 内嵌 WebEngine 很常见),映射关系大概率就在这些 JS 里。

直接全局搜 E-s:

1
grep -rln "E-s" "/Applications/京麦.app/Contents/Frameworks/咚咚工作台.app/Contents/Resources/"

命中一批文件,其中 receptionHtml/index.23385b17.js 最可疑(命名带哈希,明显是前端构建产物)。

提取映射表

JS 是压缩过的,一行几百 KB。用 Python 取 #E-s01 附近的上下文看看:

1
2
3
4
with open('/Applications/京麦.app/.../index.23385b17.js', encoding='utf-8') as f:
content = f.read()
idx = content.find('#E-s01')
print(content[max(0,idx-300):idx+800])

输出一段关键代码:

1
2
3
4
5
6
7
const d1e = {emoji: /#E-[a-z]\d{2}/};
let f1e = [
["#E-s01","[爱心]"],
["#E-s02","[安慰]"],
["#E-s03","[鄙视]"],
...
];

一箭双雕:

  1. 拿到了客户端识别表情的正则 /#E-[a-z]\d{2}/
  2. 拿到了完整映射表:code → [中文名]

Unicode 转义就是中文。爱心 = 爱心,安慰 = 安慰,以此类推。

正则一把梭

写个脚本把所有映射掏出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re, codecs, json

with open('/Applications/京麦.app/.../index.23385b17.js',
encoding='utf-8', errors='ignore') as f:
content = f.read()

pattern = r'\["(#E-[a-z0-9]+)","(\[[^\]]+\])"\]'
result = {}
for code, name in re.findall(pattern, content):
decoded = codecs.decode(name, 'unicode_escape')
result[code] = decoded

print(f'共 {len(result)} 条')
json.dump(result, open('jd_emoji_map.json', 'w', encoding='utf-8'),
ensure_ascii=False, indent=2)

执行完拿到 105 条完整映射表,分 3 套前缀:

  • s 系列 74 个:主力表情(微笑、大哭、拜拜、…)
  • b 系列 25 个:手势/业务相关(抱拳、京豆、优惠券、…)
  • a 系列 6 个:动态表情(给力、庆祝、红包、…)

前端接入

拿到 JSON 之后,在 React 项目里搞个工具文件,然后在消息渲染组件里挂上就行。

src/lib/jdEmoji.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const JD_EMOJI_PATTERN = /#E-[a-z]\d{2}/g

export const JD_EMOJI_MAP: Record<string, string> = {
'#E-s01': '[爱心]',
'#E-s04': '[大哭]',
'#E-s31': '[微笑]',
'#E-s38': '[拜拜]',
'#E-s94': '[点赞]',
'#E-b07': '[好的]',
'#E-b12': '[比心]',
// ... 共 105 条
}

export function replaceJdEmoji(text: string): string {
if (!text) return text
return text.replace(JD_EMOJI_PATTERN,
(code) => JD_EMOJI_MAP[code] ?? '[表情]')
}

注意 fallback 的设计:如果将来京东加了新表情(比如 #E-s95),没命中映射表就统一降级为 [表情],不会把原始的 #E-xxx 漏给用户看。

消息气泡里接入:

1
2
3
4
5
// MessageItem.tsx
import { replaceJdEmoji } from '@/lib/jdEmoji'

const displayContent = specialText
|| (imageMessageUrl ? content : replaceJdEmoji(content))

顺序上要注意:图片消息的特殊格式(\P(url))不能走表情替换,否则 URL 里万一有字符匹配了正则就会误伤。先判断是不是图片,再做替换。

几点总结

一、客户端渲染的 = 客户端本地有数据。任何你在客户端能看到正确渲染的东西,资源必然在本地某处,只是翻得勤不勤。表情、图标、音效、甚至有些业务枚举都是这个套路。

二、先定应用类型再动手。Electron 的 app.asar 解压即源码;Qt 的 .app 需要找 Resources 目录和 qrc 资源,可能还需要 stringsgrep 配合;Flutter 就得动 libflutter.sosnapshot 了,那个难很多。京麦是 Qt + WebEngine 的混合架构,所以最终映射表落在了 JS 文件里,反而比纯 Qt 好扒。

三、逆向思路要顺着渲染管线走。客户端识别表情 → 渲染图片,那映射逻辑必然在”识别”和”渲染”之间。grep 一个已知的表情代码 #E-s01,就能快速定位到逻辑代码,比盲目读代码快得多。

四、给未知留 fallback。扒出来的映射表是”此刻”的版本,京东随时可能加新表情。生产代码里必须有降级策略,别硬编码假设所有 code 都在表里。

最后感谢京东客户端开发团队没做代码混淆(至少变量名是 f1e 但数据结构是纯明文),否则这事情得多花几个小时。


逆向京麦客户端提取京东聊天表情映射表
https://lililib.github.io/逆向京麦客户端提取京东聊天表情映射表/
作者
煨酒小童
发布于
2026年4月29日
许可协议