Skip to content
cccskills
FreePublished on GitHub

jmcomic-apk-mod

JMComic3 APK 逆向修改:输入 APK 文件,自动解包→去广告→移除游戏/电影板块。支持 AI 可执行的意图解析+自定义组合。基于 v2.0.29 实战经验。

触发词:JMComic去广告、APK修改、去除游戏电影、jmcomic mod

The English summary is being prepared. Showing the original description.

Works withGitHub
See how to install

Files included (1)

  • SKILL.md35.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

JMComic3 APK 逆向修改技能

基于 JMComic3 v2.0.29(React + Webpack 架构)的 APK 逆向修改实战经验。

使用流程:

  1. 用户提供 APK → 解包
  2. 向用户提问,确认需求 → 意图解析 → 输出步骤清单
  3. 用户确认后 → 逐步执行修改
  4. 验证通过 → 重新打包

原则:宁可多留一个文件,不可误删一个模块。 每步操作前先验证,操作后立即检查。


架构概览

JMComic3 是一个 React SPA 应用,打包为 Android APK。理解以下三个核心概念即可上手:

1. Webpack 分块系统

代码被拆分成多个 .chunk.js 文件,按需加载:

机制语法作用
加载依赖n.e(CHUNK_ID)异步加载一个 chunk 文件
绑定模块n.bind(n, MODULE_ID)返回模块的导出内容
懒加载组件a.lazy(...)React.lazy,路由导航时才触发加载

2. 广告系统

广告通过两层机制运作:

Module 8038(广告组件,v2.0.29 ID,跨版本可能变化)
  └── 接收 adKey(如 "app_home_top")渲染广告位

Module 8284(文字链接,v2.0.29 ID,跨版本可能变化)
  └── 通过 first_links 数据生成页面顶部文字广告

3. 关键文件

以下按功能角色描述,非固定文件名。Chunk ID 在跨版本时保持不变的可能性较高,但 hash 部分每版必变。

角色识别方式定位
主入口文件名 main.*.js(不含 chunk)路由表、lazy 组件、Redux store、chunk map
App Shell体积最大的 chunk,含首页/闪屏/确认页逻辑首页渲染、确认页弹窗、闪屏页
共享模块 1含底部导航栏定义 + Module 8038(广告组件)导航栏、广告渲染、设置页
共享模块 2含底部导航栏变体 + Module 8284(文字链接)导航栏变体、文字链接广告
搜索页面含 path:"/search" 路由引用的 chunk绝对不能删!
漫画详情页含 app_detail_tab_bottom 相关广告位的 chunk详情页广告
导航栏变体 3/4与共享模块 2 类似的导航栏重复定义同一份 tab 配置的副本

自查方法:用 grep "n\.bind(n," main.*.js 列出所有模块绑定,反向追踪每个 chunk ID 的功能归属。不想猜的话,用「侦查七步法」。


通用前置步骤

0. 解包 APK

用户只需提供一个 .apk 文件,后续全部自动化。

# 用户提供的 APK 路径(唯一需要用户指定的变量)
APK_FILE="/path/to/JMComic3_v2.0.29.apk"
WORK_DIR="/path/to/JMComic3v2.0.29"

# 解包(APK 本质是 ZIP 文件)
mkdir -p "$WORK_DIR"
unzip -o "$APK_FILE" -d "$WORK_DIR"
cd "$WORK_DIR"

APK 内部结构速览:classes.dex(Java/Kotlin 字节码)、resources.arsc(编译后的资源表)、assets/(Web 前端文件,我们主要修改这个目录)、res/(Android 原生资源)、META-INF/(签名信息)、AndroidManifest.xml(清单文件)。

1. 需求确认(必须执行,不可跳过)

不要假设用户想要什么。 解包完成后,根据用户的初始表述和以下维度,主动向用户提问确认:

询问维度:

维度示例问题
广告范围"要去掉所有广告,还是只去掉特定类型的(闪屏/banner/文字链接)?"
板块去留"游戏和电影板块要保留、删除其中一部分、还是全部删除?"
修改方式"是否需要 Git 版本管理?还是直接改文件就行?"
打包方式"修改完需要重新打包成 APK 吗?推荐使用 GitHub Actions 在线打包,无需安装 Android SDK(约 1GB),零本地环境依赖。"
打包类型"需要哪种 APK?Debug(调试版,~25MB,可直接安装)/ Release(发布版,~25MB,需签名)/ 两种都要?"

问法示例(用户说"帮我去广告"时):

了解。在开始之前确认几个细节:

1. 广告范围 — 所有广告都去?还是保留某些(如闪屏)?
2. 板块去留 — 游戏和电影板块要不要一起删掉?
3. 版本管理 — 需要 Git 追踪修改记录,还是直接改文件就行?
4. 打包方式 — 推荐 GitHub Actions 在线打包(无需装 Android SDK),还是本地打包?
5. 打包类型 — Debug(可直接安装)/ Release(需签名)/ 都要?

根据用户回答,运行「意图解析流程」生成步骤清单,展示给用户确认后再动手。

2. 工作准备

# 解包后进入工作目录
cd "$WORK_DIR"

方式一:Git 版本管理(推荐—可回滚)

git init && git add -A && git commit -m "原始版本"

方式二:直接修改(无 Git—适合一次性操作)

无需额外操作。建议自行备份原始文件:

cp -r assets/public/static/js assets/public/static/js.bak

Git 方式适合多步修改和回滚;无 Git 方式适合"只改一两处就行"的场景。由用户决定。

搜索速查表

目标搜索模式示例
路由path:"/xxx"path:"/search"
懒加载组件a.lazy(...n.bind(n,ID)n.bind(n,5884) → 搜索页
广告位adKey:"xxx"adKey:"app_home_top"
文字链接数据first_links8 个 chunk 中含此模式
免广告标志ad_free改为 !0
Chunk 引用n.e(ID)n.e(5884) 加载搜索 chunk
模块绑定n.bind(n,ID)n.bind(n,5884) 绑定搜索模块

安全守则(9 条)

  1. 永远精确字符串替换,不用正则
  2. 修改前搜索确认作用域,一处还是多处
  3. 修改前检查敏感上下文(Cookie、主题、阅读器 — 见下方)
  4. 每小步验证后再继续(Git 模式:改前 commit 改后 grep;无 Git 模式:改前备份文件)
  5. 删 chunk 前双重确认:n.e() 和 n.bind() 无残留引用
  6. 共享代码全量搜索,不遗漏任何重复 chunk
  7. 不用 git add -A,指定具体文件
  8. .map 分析完再删,它是唯一的源码线索
  9. 不确定的 chunk 保留不删

禁止触碰的区域

以下 6 类代码与广告系统完全隔离,绝对不要修改:

#区域识别关键词误改后果
1Cookie/登录态cookie, AVS, token, session登录丢失、进度丢失
2亮暗色模式theme, dark, light, DarkMode主题切换失效
3阅读器核心reader, viewer, scroll, zoom翻页/缩放异常
4Redux StoredarkMode, user, settings(ad_free 除外)全局状态异常
5Android 桥Android, WebView, bridge, JavascriptInterface原生功能失效
6网络/APIfetch(, axios, .php, api, baseURL数据加载失败

修改前安全检查:

# 检查目标代码周围 200 字符是否触碰敏感区域
grep -oP '.{0,200}要删除的字符串.{0,200}' target.js \
  | grep -iE 'cookie|token|theme|dark|reader|bridge|android|fetch'
# 有输出 → 暂停审查 / 无输出 → 安全继续

黄金法则:看不懂的 minified 代码不要动。宁可多问,不可乱改。


自定义需求指南(AI 执行手册)

本章为 AI 提供一套机械化的意图解析流程——接收用户的自然语言需求,输出一个无歧义的步骤清单。

意图解析流程

用户输入(自然语言)
  → 1. 关键词提取:匹配下方的「意图→步骤」决策表
  → 2. 步骤收集:合并所有匹配到的步骤 ID
  → 3. 冲突检测:检查是否有互斥操作(如"删电影"+"保留电影")
  → 4. 排序去重:按执行顺序排列(A1→A10 → B1→B6)
  → 5. 输出清单:向用户确认后再执行

意图 → 步骤决策表

解析用户请求时,按以下规则逐条匹配。每条命中就收集对应的步骤 ID。多命中合并,不互斥。

用户意图关键词匹配逻辑收集步骤
去广告 / 移除广告 / 无广告 / 去所有广告全广告清除A1, A2, A3, A4, A5, A6, A7, A8, A9
banner / 顶部广告 / 首页广告仅首页广告位A2
闪屏 / 启动广告 / 开屏仅闪屏A4
插屏 / 路由广告 / 跳转广告仅路由插屏A3
浮动 / 悬浮 / 底部广告仅浮动广告A3
文字链接 / 文字广告 / 顶部链接仅文字链接A7
详情页广告 / 漫画页广告仅详情页A5
搜索页广告 / 搜索底部仅搜索页A6
弹窗 / 确认页 / popup仅弹窗A8
保留闪屏 / 不要删闪屏 / 闪屏留着排除闪屏从已收集步骤中移除 A4
保留banner / 不要删banner排除 banner从已收集步骤中移除 A2
去游戏 / 移除游戏 / 删游戏 / 不要游戏删游戏板块B1_games, B2_games, B3, B4, B5
去电影 / 移除电影 / 删电影 / 不要电影删电影板块B1_movies, B2_movies, B3, B4, B5
去板块 / 移除板块 / 去游戏和电影 / 全删删全部板块B1, B2, B3, B4, B5
保留游戏 / 不要删游戏排除游戏从已收集步骤中移除所有 B_games
保留电影 / 不要删电影排除电影从已收集步骤中移除所有 B_movies
只侦查 / 研究结构 / 分析 / 不修改仅侦查侦查七步法(不执行任何修改步骤)
不改chunk / 不删文件 / 只屏蔽渲染 / 保守保守模式收集广告步骤,但跳过 B3/B4/B5(不删 chunk)

步骤排程规则

收集到步骤集合后,按以下顺序排列执行:

优先级: A1 > A2 > A3 > A4 > A5 > A6 > A7 > A8 > A9 > B1 > B2 > B3 > B4 > B5 > 删.map

互斥处理:

  • 如果同时命中「去某板块」和「保留该板块」→ 保留优先,不执行删除
  • 如果用户只说了"去广告"未提板块 → 默认不执行任何 B 步骤
  • 如果执行了任何 B 步骤 → B3/B4/B5 必须配套执行(孤儿清理是强制依赖)

隐式依赖:

  • 执行任何 A 步骤 → 隐含需先执行 A1(ad_free 是全局基础)
  • 执行任何 B 步骤 → 隐含需先完成所有 A 步骤(去广告是去板块的前提)
  • 执行 B3 → 必须在 B1+B2 之后
  • 执行 B5 → 必须在 B4 之后

adKey → 步骤映射

每个广告位对应一个修改步骤:

广告位位置步骤页面
app_home_top首页顶部 bannerA2首页
app_home_float首页浮动A3首页
app_route路由插屏A3全局跳转
app_splash闪屏1A4启动
app_splash2闪屏2A4启动
app_splash_bottom闪屏底部A4启动
app_detail_tab_bottom_jm3详情页底部A5漫画详情
app_detail_introduction_bottom_jm3详情简介底部A5漫画详情
app_search_bottom_jm3搜索页底部A6搜索
app_movies_top_banner电影页顶部A2(相同模式)电影(有板块时)
app_movies_fixed_bottom_jm3电影页底部A3(相同模式)电影(有板块时)
app_categories_bottom_jm3分类页底部A3(相同模式)分类(有板块时)
页面顶部文字链接全局A7所有页面

功能移除 → 步骤映射

目标步骤说明
移除游戏B1_games + B2_games + B3 + B4 + B5仅删 /games 路由及孤儿 chunk
移除电影B1_movies + B2_movies + B3 + B4 + B5仅删 /movies、/movies/:id 路由及孤儿 chunk
全部移除B1 + B2 + B3 + B4 + B5游戏+电影全删

解析示例

用户: "帮我去掉banner和文字链接,电影板块也不要了,但游戏留着"
  → 关键词匹配: banner=A2, 文字链接=A7, 去电影=B1_movies+B2_movies+B3+B4+B5, 保留游戏=排除B_games
  → 隐式依赖: A2→需A1, B步骤→需全部A步骤
  → 最终清单: A1, A2, A3, A4, A5, A6, A7, A8, A9, B1_movies, B2_movies, B3, B4, B5
  → 去重排序后执行

用户: "只去掉闪屏广告,其他都不要动"
  → 关键词: 闪屏=A4
  → 隐式依赖: A4→需A1
  → 最终清单: A1, A4

用户: "分析一下这个新版本的广告结构,先不改"
  → 关键词: 分析+不修改
  → 最终清单: 侦查七步法

场景 A:去广告

适用场景:保留游戏/电影板块,清除所有广告。 以下所有代码块均为 v2.0.29 示例,用于展示修改模式。新版本中具体的 chunk ID、变量名、替换字符串可能不同——请先执行「侦查七步法」定位,再按相同模式修改。

A1. 全局 ad_free 标志

文件:main.*.js | 难度:★ | 风险:低

# v2.0.29 示例
("ad_free:!1", "ad_free:!0", "全局免广告")

将 Redux store 中的 ad_free 强制为 true。广告组件渲染前会检查此标志。

自查:grep "ad_free" main.*.js,找到 ad_free:!1 或 ad_free:!0 附近的精确字符串。

A2. 首页顶部 Banner

文件:含 app_home_top 的 chunk | 难度:★★ | 风险:低

# v2.0.29 示例 — 新版本需先 grep "app_home_top" 定位实际字符串
("(0,N.jsx)(E.A,{adKey:\"app_home_top\",adIndex:e})", "null", "首页 banner")
("bannerList:null", "bannerList:[]", "banner 数据源")

自查:grep -l "app_home_top" assets/public/static/js/*.js 定位文件,再 grep -oP '.{0,80}app_home_top.{0,80}' 提取精确替换字符串。

A3. 路由插屏 + 首页浮动

文件:含 app_route 的 chunk(通常与 A2 同一文件) | 难度:★ | 风险:低

# v2.0.29 示例
("(0,N.jsx)(E.A,{adKey:\"app_route\"})", "null", "路由插屏")
("(0,N.jsx)(E.A,{adKey:\"app_home_float\"})", "null", "首页浮动")

自查:grep -oP '.{0,80}app_route.{0,80}' 和 grep -oP '.{0,80}app_home_float.{0,80}' 提取替换字符串。

A4. 闪屏广告

文件:含 app_splash 的 chunk | 难度:★★ | 风险:中

# v2.0.29 示例
("[\"app_splash\",\"app_splash2\",\"app_splash_bottom\"].map(", "[].map(", "闪屏数据源")

自查:grep -oP '.{0,120}\"app_splash\".{0,120}' 提取完整的数据源数组,改为空数组。额外搜索启动页渲染逻辑中的广告索引(通常为数字 3),确认跳过。

A5. 漫画详情页

文件:含 app_detail_tab_bottom 的 chunk | 难度:★★ | 风险:低

# v2.0.29 示例 — 将广告 JSX 渲染替换为 null
("(0,N.jsx)(E.A,{adKey:\"app_detail_tab_bottom_jm3\"})", "null", "详情页底部")
("(0,N.jsx)(E.A,{adKey:\"app_detail_introduction_bottom_jm3\"})", "null", "详情简介底部")

自查:grep -l "app_detail" assets/public/static/js/*.js 定位文件,再用 grep -oP '.{0,80}app_detail.{0,80}' 提取精确替换字符串。

A6. 搜索页底部

文件:含 app_search_bottom 的 chunk(即搜索页面 chunk) | 难度:★ | 风险:低

# v2.0.29 示例
("adKey:\"app_search_bottom_jm3\"", "adKey:\"\"", "搜索页底部广告")

⚠️ 此 chunk 是搜索页面的核心。只改广告渲染,绝不删文件。

自查:grep -l "app_search_bottom" assets/public/static/js/*.js 定位,提取 JSX 替换为 null。

A7. 顶部文字链接(关键步骤)

文件:所有含 first_links 的 chunk | 难度:★★★ | 风险:低

广告链:first_links 数据 → exchange_link 组件 → 页面顶部

操作:

# 1. 找到所有涉及的 chunk
grep -rl "first_links" assets/public/static/js/*.js | grep -v ".map"

在每个命中文件中执行:

# v2.0.29 示例 — 新版本需 grep 确认实际字符串格式
("return[...X.first_links||[],...Y]", "return[]", "first_links → 空")

额外处理 exchange_link(通常在含 Module 8284 的 chunk 中):

# v2.0.29 示例
("exchange_link:...something...", "exchange_link:b([])", "exchange_link 清空")

自查:grep -oP '.{0,60}first_links.{0,60}' 在每个命中文件中提取精确替换字符串。v2.0.29 涉及 8 个文件,新版本可能不同。

A8. 确认页弹窗

文件:含 splash_top 或 pop1_list 的 chunk(通常与 A2 同一文件) | 难度:★★ | 风险:中

# v2.0.29 示例
("splash_top:...", "splash_top:[]", "弹窗数据源")

自查:grep -oP '.{0,80}pop1_list|splash_top.{0,80}' 定位弹窗数据源,清空对应数组。

A9. ad_free 补丁确认

文件:含 Module 8038 的 chunk(共享模块 1) | 难度:★ | 风险:低

确认广告组件中的 ad_free 检查逻辑已生效。v2.0.29 中对应 L=!0——新版本中变量名会变,需搜索 ad_free 在 chunk 文件中的出现位置确认。

自查:grep "ad_free" assets/public/static/js/*.chunk.js,确认广告组件中该标志被强制为 true。

A10. 验证

[ ] 首页无 banner / 浮动广告
[ ] 各页面无顶部文字链接
[ ] 漫画详情页无广告
[ ] 搜索页无底部广告
[ ] 无闪屏(启动直达主页)
[ ] 路由跳转无插屏
[ ] 游戏/电影板块正常
[ ] grep -r "adKey" 返回条目均已处理
[ ] 核心功能:暗色模式 ✓ 登录态 ✓ 阅读器 ✓ 搜索 ✓ 下载 ✓

场景 B:去广告 + 去板块

前提:场景 A 所有步骤已完成并验证通过。 以下所有代码块和 chunk ID 均为 v2.0.29 示例。新版本中 chunk ID、路由格式、lazy 组件变量名均可能变化——请先用「侦查七步法」定位,再按相同模式修改。

B1. 移除路由

文件:main.*.js | 难度:★★ | 风险:中

// v2.0.29 示例 — 新版本用 grep 'path:"/games"\|path:"/movies"' main.*.js 提取
(0,Se.jsx)(e.qh,{path:"/games",element:...}),
(0,Se.jsx)(e.qh,{path:"/movies",element:...}),
(0,Se.jsx)(e.qh,{path:"/movies/:id",element:...}),

自查:grep -oP '.{0,100}path:"/(games|movies)[^"]*".{0,100}' main.*.js 提取精确的路由定义行,逐个删除。如果用户只要删游戏或只要删电影,只删对应路由。

B2. 移除 Tab 入口

文件:5 个 chunk | 难度:★★★ | 风险:低

# 找到所有含 tab 定义的 chunk
grep -rl '"games"\|"movies"' assets/public/static/js/*.js | grep -v ".map"

在每个命中的 chunk 中删除如下条目:

// v2.0.29 示例 — 新版本 grep -oP '.{0,80}"games".{0,80}' 提取
{icon:...,nav:"games",label:x("nav.game"),link:"/games"},    // 删除
{icon:...,nav:"movies",label:x("nav.movie"),link:"/movies"},   // 删除

自查:先 grep -rl '"games"\|"movies"' 找到所有文件,再逐个 grep -oP '.{0,80}"games".{0,80}' 提取精确行删除。文件数量每版不同,v2.0.29 涉及 5 个。

B3. 识别孤儿组件

路由删除后,对应的 lazy 组件不再被使用。用脚本自动识别:

MAIN_JS=$(ls assets/public/static/js/main.*.js | head -1)
python3 -c "
import re
with open('$MAIN_JS') as f: content = f.read()
lazy = {m.group(1): m.group(3) for m in re.finditer(r'(\w+)=a\.lazy\(\(\)=>Promise\.all\(\[([^\]]*)\]\)\.then\(n\.bind\(n,(\d+)\)\)\)', content)}
route_comps = set(re.findall(r'element:\(0,\w+\.jsx\)\((\w+)', content))
for name, cid in lazy.items():
    if name not in route_comps:
        print(f'  孤儿 → {name} (chunk {cid})')
"

v2.0.29 孤儿示例(仅供理解输出格式):ze(7521 电影列表)、De(6120 电影播放器)、Fe(8063 游戏页/依赖 2846)。

自查:脚本自动运行,无需手动猜测。

B4. 清理 main.js

文件:main.*.js | 难度:★★ | 风险:中

  1. 删除上一步识别的每个孤儿 lazy 定义行
  2. 删除 chunk map 中对应条目(如 ,2846:"861c1e38")

B5. 删除孤儿 chunk

删除前确认(不可省略):

# CHUNK_IDS 替换为 B3 脚本输出的 chunk ID 列表
CHUNK_IDS="2846 6120 7521"  # v2.0.29 示例
for ID in $CHUNK_IDS; do
  echo "=== chunk $ID ==="
  grep -r "n\.e($ID)" assets/public/static/js/ | grep -v ".map"  # 必须空
  grep -r "n\.bind(n,$ID)" assets/public/static/js/ | grep -v ".map"  # 必须空
done

确认两项均为空后,删除对应的 .js 和 .map 文件。

自查:Chunk ID 列表来自 B3 脚本输出,不手动硬编码。

B6. 验证

[ ] 场景 A 全部通过(含核心功能)
[ ] 底部导航无"游戏""电影"
[ ] 搜索正常(无白屏)
[ ] 无残留 n.e() 引用
[ ] 所有页面路由跳转正常
[ ] 暗色模式 / 登录态 / 阅读进度 正常

内容注入(扩展思路)

用户可能想在去广告的同时往 APK 里加入自己的内容,常见场景如:

  • 把广告位替换为自己的推广图片
  • 替换启动图/图标
  • 注入自定义 CSS 或 JS 脚本

通用思路

由于面对的是 minified Webpack 产物,无法直接"加一段代码",但可利用已有入口:

图片类替换(最稳妥):

  • 广告位被替换为 null 后,该区域在 DOM 中消失。若用户想在此位置放自己的图片,不需要改 JS——在 index.html 中注入一个固定定位的 <img> 标签即可。
  • 资源文件直接放入 assets/ 目录,重新打包时自动包含。

启动图/图标替换:

  • res/mipmap-* 和 res/drawable-* 文件夹包含启动图和图标,替换同名文件即可。
  • 已在 v2.0.29 实战中验证。

JS/CSS 注入(需谨慎):

  • 安全方式:在 assets/public/index.html 中追加 <script> 或 <style> 标签。
  • chunk 内注入:不推荐(作用域风险)。

询问与确认

当用户提出"想加点东西"时,按以下顺序确认:

  1. 加什么 — 图片、CSS、JS 脚本?
  2. 加在哪里 — 哪个页面、什么位置?
  3. 触发条件 — 始终显示还是特定条件?

然后坦诚告知:

  • 能做到的 → 给出方案
  • 需要侦查才能确认的 → 先侦查再答复
  • 当前架构下不支持的 → 直接说明,不画饼

本节为开放思路,具体实现取决于用户需求和实际代码结构。基本原则:能做就做,不能做就说,不强行编造方案。


新版本适配流程

JMComic3 是上游闭源项目。新版本中文件名 hash 和变量名可能变化,但核心架构(Webpack + React Router + 广告机制)不变。

侦查七步法

接手新版本 APK 时,不要急于修改,按顺序侦查:

1. 保留 .map,用它了解项目

python3 -c "
import json, glob
for f in glob.glob('assets/public/static/js/main.*.js.map'):
    with open(f) as fp:
        data = json.load(fp)
    for s in data.get('sources', []): print(s)
" 2>/dev/null | head -30

2. 提取路由表 + 组件→chunk 映射

MAIN_JS=$(ls assets/public/static/js/main.*.js | head -1)
python3 -c "
import re
with open('$MAIN_JS') as f: content = f.read()
for m in re.finditer(r'path:\\\"([^\\\"]+)\\\"', content):
    ctx = content[m.start():m.start()+200]
    elem = re.search(r'element:\(0,\w+\.jsx\)\((\w+)', ctx)
    print(f'/{m.group(1):30s} → {elem.group(1) if elem else \"?\"}')
"

3. 扫描 adKey

grep -roh 'adKey:"[^"]*"' assets/public/static/js/*.js | sort -u

对照上文 adKey 映射表,确认哪些是新增的。

4. 定位 first_links 分布

grep -rl "first_links" assets/public/static/js/*.js

5. 定位 Tab 栏

grep -rl '"games"\|"movies"' assets/public/static/js/*.js

6. 建立 chunk 引用关系

for id in $(grep -oh 'n\.e(\d\+)' "$MAIN_JS" | grep -o '\d\+' | sort -u); do
  refs=$(grep -l "n\.e($id)\|n\.bind(n,$id)" assets/public/static/js/*.js | wc -l)
  echo "chunk $id: $refs 处引用"
done

7. 全部完成后删 .map

find assets/public -name "*.map" -type f -delete

跨版本自查原则

文档中 # v2.0.29 示例 的代码块在跨版本时可能与实际代码不同,也可能完全相同——取决于上游是否改动了广告相关逻辑。不要预设一定会变或一定不变。

遇到 Python batch_replace 返回 FAIL 时,按以下优先级自行探索:

P0 — 先扫描 adKey(最稳定的特征):

grep -roh 'adKey:"[^"]*"' assets/public/static/js/*.js | sort -u

对照已知 adKey 映射表,快速判断哪些广告位还在、哪些新增、哪些消失。

P1 — 字符串不匹配 → 扩大搜索窗口:

# 截取关键词周围上下文,观察新版本的渲染格式
grep -oP '.{0,150}app_home_top.{0,150}' target_chunk.js
# 从上下文中提取新的精确替换字符串,更新替换元组

P2 — 文件定位失败 → 特征搜索代替 ID 搜索:

  • 不找 6809.*.chunk.js → 找含 app_home_top 的 chunk
  • 不找 1791.*.chunk.js → 找含 ad_free 和 JSX 渲染的 chunk
  • 不找 7451.*.chunk.js → 找含 app_detail 的 chunk

P3 — 变量名变化 → 追踪语义等价:

  • L=!0 可能变成 M=!0 或 adFree=!0
  • E.A(JSX 工厂)可能变成 s.jsx 或 r.createElement
  • 关键:不找变量名,找功能语义——广告组件始终接收 adKey 参数

P4 — 新增未知广告位 → 自行推断,而非直接问用户:

grep -roh 'adKey:"[^"]*"' assets/public/static/js/*.js | sort -u | while read key; do
  echo "$key → $(grep -l "$key" assets/public/static/js/*.js)"
done

不在已知映射表中的 adKey:

  1. 观察其渲染格式——是否与已知广告位(如 app_home_top)共用同一组件
  2. 在 .map 文件中搜索该 adKey 上下文,确认所属页面
  3. 参考最相似的已知广告位处理方式(JSX 替换为 null / 数据源清空)
  4. 自行决定处理方式并执行——大多数情况下新模式只是旧模式的变体

P5 — 以上路径均走不通 → 换思路,不放弃:

  • 检查是否因为 chunk hash 变化导致在错误文件中搜索 → 扩大 grep -rl 范围到整个 js/ 目录
  • 检查是否上游重构了广告架构(如从 Webpack Module 改为动态导入)→ 对比 .map 的 sources 列表
  • 检查是否广告位被改名(如 app_home_top → app_main_top)→ 搜索 adKey 附近的上下文语义
  • 已成功定位的步骤可以先执行,失败的步骤留到下一轮单独攻克

P6 — 所有路径穷尽后: "以下广告位在当前版本中与其他已知模式差异较大,暂时未能定位:xxx。请问是否需要:1) 我继续深入分析 2) 跳过这些,先执行已定位的步骤 3) 您手动定位后告诉我具体位置"

核心原则:先自行探索,多路径尝试。绝大多数跨版本差异只是变量名和 hash 的变化,结构不变。实在穷尽手段后再告知用户,而非一遇 FAIL 就求助。


实战避坑

以下来自 v2.0.29 真实踩坑记录:

#现象根因教训
1搜索白屏误删 5884(它其实是搜索页,不是电影)删 chunk 前必须查 n.bind() 确认用途
2Tab 仍在Tab 栏在 5 个 chunk 中重复定义共享 UI 全量 grep -rl 搜索
3文字链接复现first_links 在 8 个 chunk,只补了 2 个不凭感觉,grep -rl 找全
4APK 变大git add -A 提交了 79MB 构建产物只用 git add <file>
5分析困难.map 删太早,后续无法回溯.map 留到验证通过

广告追踪的核心思路

用户看到的广告位置
  → 搜索 minified JS 中的渲染代码
  → 找到 adKey(如 "app_home_top")
  → 追踪渲染函数 → Module 8038
  → 处理策略:
      直接渲染 → JSX 替换为 null
      列表生成 → 清空数据源
      全局控制 → ad_free 强制 true
  → 顽固广告(文字链接):
      追踪 Module 8284 → first_links → 全量搜索 → 逐个修补

APK 体积优化

删除 .map 文件

只在所有修改验证通过后执行。

find assets/public -name "*.map" -type f -delete
git ls-files --deleted | grep "\.map$" | xargs git rm
echo "*.map" >> .gitignore

体积对比(v2.0.29):28.9 MB → 25.1 MB(-3.8 MB)


构建与部署

修改完成后,将修改后的文件重新打包为 APK。

打包方式选择

推荐:GitHub Actions 在线打包 — 无需安装 Android SDK(约 1GB+),零本地环境依赖,适用于所有用户。

本地打包 — 需安装 JDK 17+ 和 Android SDK Build-Tools,适用于开发者。

在需求确认阶段就应告知用户这个选项。大多数用户选择 GitHub Actions 即可。

APK 类型说明

类型大小签名用途
Debug APK~25MB自动 debug 签名直接安装测试,无需配置签名。适合自己用
Release APK~25MB需提供 keystore正式发布用。未签名或自签名的 release APK 也可直接安装
两者都要~50MB 总计-同时产出 debug 和 release 版本

Debug 和 Release 体积几乎相同,区别仅在于签名方式。大部分用户选 Debug 即可。

方式一:GitHub Actions 在线打包(推荐)

将修改推送到 GitHub 仓库后,Actions 自动构建。用户只需从 Artifacts 下载 APK。

# .github/workflows/build-apk.yml
name: Build APK
on:
  push:
    branches: [main, master]
  workflow_dispatch:
    inputs:
      build_type:
        description: 'APK 类型'
        required: true
        type: choice
        options: [debug, release, both]
        default: debug

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: {java-version: '17', distribution: 'temurin'}
      - uses: android-actions/setup-android@v3

      - name: Package APK (Debug)
        if: ${{ github.event.inputs.build_type == 'debug' || github.event.inputs.build_type == 'both' }}
        run: |
          rm -f META-INF/CERT.RSA META-INF/CERT.SF META-INF/MANIFEST.MF
          zip -D -0 app-debug-unsigned.apk resources.arsc classes.dex DebugProbesKt.bin AndroidManifest.xml
          zip -r -D -n ".png:.jpg:.jpeg:.gif:.webp:.mp3:.mp4:.ogg:.dex" \
            app-debug-unsigned.apk assets/ kotlin/ META-INF/ org/ res/ \
            -x "*.DS_Store" "__MACOSX/*" ".git/*" ".github/*" ".gitignore" "*.apk" "*.keystore" "skills/*"
          keytool -genkey -keystore debug.keystore -alias androiddebugkey \
            -keyalg RSA -keysize 2048 -validity 10000 \
            -storepass android -keypass android \
            -dname "CN=Debug, OU=Dev, O=JM" -noprompt
          $ANDROID_SDK_ROOT/build-tools/34.0.0/zipalign -v -p 4 app-debug-unsigned.apk app-debug.apk
          $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \
            --ks debug.keystore --ks-pass pass:android \
            --out JMComic3-debug.apk app-debug.apk

      - name: Package APK (Release)
        if: ${{ github.event.inputs.build_type == 'release' || github.event.inputs.build_type == 'both' }}
        run: |
          rm -f META-INF/CERT.RSA META-INF/CERT.SF META-INF/MANIFEST.MF
          zip -D -0 app-release-unsigned.apk resources.arsc classes.dex DebugProbesKt.bin AndroidManifest.xml
          zip -r -D -n ".png:.jpg:.jpeg:.gif:.webp:.mp3:.mp4:.ogg:.dex" \
            app-release-unsigned.apk assets/ kotlin/ META-INF/ org/ res/ \
            -x "*.DS_Store" "__MACOSX/*" ".git/*" ".github/*" ".gitignore" "*.apk" "*.keystore" "skills/*"
          $ANDROID_SDK_ROOT/build-tools/34.0.0/zipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk
          $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \
            --ks release.keystore --ks-pass pass:${{ secrets.KEYSTORE_PASS }} \
            --out JMComic3-release.apk app-release-aligned.apk

      - uses: actions/upload-artifact@v4
        if: ${{ github.event.inputs.build_type == 'debug' || github.event.inputs.build_type == 'both' }}
        with: {name: JMComic3-debug, path: JMComic3-debug.apk}
      - uses: actions/upload-artifact@v4
        if: ${{ github.event.inputs.build_type == 'release' || github.event.inputs.build_type == 'both' }}
        with: {name: JMComic3-release, path: JMComic3-release.apk}

首次使用 GitHub Actions 打包前的操作:

  1. 将项目推送到 GitHub 仓库
  2. 将上述 YAML 写入 .github/workflows/build-apk.yml
  3. 在仓库 Settings → Secrets 中添加 KEYSTORE_PASS(仅 release 需要)
  4. 进入 Actions 页面,手动触发 Build APK workflow,选择 build_type
  5. 等待 2-3 分钟,从 Artifacts 下载 APK

方式二:本地打包

需要本地安装 JDK 17+ 和 Android SDK Build-Tools。适用于开发者或网络受限场景。

# Debug APK(自动签名,可直接安装)
cd "$WORK_DIR"
rm -f META-INF/CERT.RSA META-INF/CERT.SF META-INF/MANIFEST.MF
zip -D -0 app-unsigned.apk resources.arsc classes.dex DebugProbesKt.bin AndroidManifest.xml
zip -r -D -n ".png:.jpg:.jpeg:.gif:.webp:.mp3:.mp4:.ogg:.dex" \
  app-unsigned.apk assets/ kotlin/ META-INF/ org/ res/ \
  -x "*.DS_Store" "__MACOSX/*" ".git/*" ".github/*" ".gitignore" "*.apk" "*.keystore" "skills/*"
keytool -genkey -keystore debug.keystore -alias androiddebugkey \
  -keyalg RSA -keysize 2048 -validity 10000 \
  -storepass android -keypass android \
  -dname "CN=Debug" -noprompt
zipalign -v -p 4 app-unsigned.apk app-aligned.apk
apksigner sign --ks debug.keystore --ks-pass pass:android \
  --out JMComic3-mod.apk app-aligned.apk
# Release APK(需提供 keystore 和密码)
# ...同上,签名时替换 keystore 路径和密码
apksigner sign --ks your-release.keystore --ks-pass pass:YOUR_PASS \
  --out JMComic3-release.apk app-aligned.apk

本地打包需 ~1GB Android SDK + ~500MB JDK,磁盘空间不足时选择 GitHub Actions。


故障排查

症状最常见原因修复
搜索白屏chunk 5884 被误删恢复 5884 chunk 文件
文字链接复现first_links 有遗漏grep -rl "first_links" 全量修补
Tab 仍在只改了部分 chunkgrep -rl '"games"' 逐个处理
APK 变大.bak/build-artifact 误提交检查 git 大文件,清理后重建
新版匹配失败变量名变了搜索 adKey / E.A 重新定位

Python 辅助脚本

"""JMComic3 APK 批量精确字符串替换"""
import sys

def batch_replace(file_path, replacements):
    """replacements: [(old_str, new_str, description), ...]"""
    with open(file_path, 'r') as f:
        content = f.read()

    ok = fail = 0
    for old, new, desc in replacements:
        if old in content:
            content = content.replace(old, new)
            print(f"  OK  {desc}")
            ok += 1
        else:
            print(f"  FAIL {desc} — 未找到!")
            fail += 1

    if fail == 0:
        with open(file_path, 'w') as f:
            f.write(content)
        print(f"完成: {ok}/{len(replacements)}")
    else:
        print(f"中断: {fail} 处失败,文件未修改")
        sys.exit(1)

版本适配速查

稳定要素说明
n.e(ID) / n.bind(n, ID)Webpack 加载机制
a.lazy(...)React.lazy 模式
adKey:"xxx"广告位标识
first_links / exchange_link文字链接数据键
ad_free免广告标志
path:"/xxx"路由路径
Chunk ID(数字)跨版本相对稳定

适配核心:不找文件名,找特征模式。先用 .map 侦查,再按模式定位,验证通过后删 .map。

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

Report a problem with this skill