好友
阅读权限 10
听众
最后登录 1970-1-1
天天跳绳 (com.gkid.crazyrope) 逆向分析全链路
版本: 4.1.14
目标: 登录后自动完成所有体育作业, 可重复刷, 不被风控检测
环境: dotOS 5.1.3 (Android 11) + Magisk v30.7 root, PC=Kali Linux
设备: Redmi Note 5, 序列号 48c0f066
日期: 2026-08-11(最终更新 18:40)
作者: Leonzane
目录
APK 基础信息
加固与反调试分析
API 逆向提取
作业完成判定机制
提交接口数据结构
自动化工具详解
全自动登录模块(手机号+验证码+滑块)
风控防护设计
工具开发迭代与踩坑
附录: 完整接口清单
1. APK 基础信息
项
值
包名
com.gkid.crazyrope
版本
4.1.14
主进程
com.gkid.crazyrope
作业游玩 Activity
com.gkid.crazyrope/.ui.drillcommon.LandDrillActivity
用户 UID
bc4eb4e3-bd80-4043-8fd2-509412c0ff07
用户 NO (formatUserNo)
0094199075
核心 API 域名
api.tiantiantiaosheng.com
旁路域名 : api.tizhiyun.com、api-manage.tiantiantiaosheng.com、cdn-data.tiantiantiaosheng.com
2. 加固与反调试分析
2.1 检测到的保护
vDexVMP / InnoSecure : 深度加固, 反调试反注入
DEX 加固 : 实际业务代码在释放后的 DEX 中
2.2 frida 注入测试结果(全部失败,已放弃 )
方式
结果
frida -U -f com.gkid.crazyrope (spawn)
早期注入可绕过 Java 未初始化错误, 但最终触发完整 Java 崩溃栈 (libart.so)
frida -U <pid> (attach)
PID 从 2336 自杀重启为 22516, 反调试检测到注入直接崩溃
REPL 常驻 hook
崩溃输出完整 Java 崩溃栈 (fatal exception / libart.so)
结论 : vDexVMP/InnoSecure 检测到 frida 注入标记后直接让 App 崩溃 (art 层 fatal exception)。frida 注入 = 必被杀 , 该路线彻底走不通, 放弃动态 hook。
2.3 绕开方案(成功路线)
不依赖动态 hook, 改用纯静态 smali 逆向 + 独立 Python 脚本直调接口。
反编译产物: /tmp/ttts_all/classes*/
关键模型: classes8/.../model/record/Record.smali (9453 行)
3. API 逆向提取
3.1 token 提取
adb shell "su -c 'cat /data/data/com.gkid.crazyrope/shared_prefs/Settings.xml'"
→ 正则 name="LogInRsp">(.*?)</string>
→ html.unescape → json.loads → d['data']['token']
token 是 JWT, 存 /tmp/gkid_token.txt
payload: {"user_id":"bc4eb4e3-...","exp":1789026877000,"code":951410}
错误码 41001 = token 截断/错误 (PC 端提取时截断导致误判"账号风控锁定",实为 token 不完整)
3.2 请求头(全套)
Authorization: Bearer <token>
x_littlelights_source: gkid
X-Gkid-appVersion: 4.1.14
X-Gkid-deviceName: Redmi Note 5
X-Gkid-systemVersion: 11
X-Gkid-uuid: <uid>
X-Gkid-flavor: normal
X-Gkid-no: 0094199075
User-Agent: okhttp/4.9.0
Content-Type: application/json; charset=UTF-8 (POST 时)
3.3 作业相关接口
接口
用途
GET /api/homework/details_v7?start_date=YYYY-MM-DD&days=N
作业列表(release+homeworks), 精简版
GET /api/homework/details
老版详情, 返回 BaseDrill 完整关联字段 (plan_day_id/train_item_id)
POST /api/user/drill_record_upload
作业记录提交接口(核心)
GET /api/user/drill_record_list_v5?page=&page_size=
历史记录
GET /api/user/today_homework_share?everyday_release_id=
服务端视角游玩统计
POST /api/course/drill_playing
单动作游玩(仅 drill_name)
POST /api/course/course_plan_playing
课程计划游玩(返回 CoursePlayingRsp)
4. 作业完成判定机制(核心破解)
4.1 关键发现线索(逆向排查过程)
提交记录能入库但未关联作业 : 用 drill_record_upload 提交带 homework_id 的记录, 记录出现但作业 status=uncompleted, current_user_play_repeats=0。
误判排除 : 排出 reset_homework(code:-1)、课程组 group_record_id(属课程计划非作业)。
认知修正 : 老板真实做切水果后 homework 848795922 变 done replay=1, 确认 drill_record_upload 确实是作业提交接口 。
服务端不靠 homework_id 字段 : 脚本带 homework_id 提交仍未关联 — 服务端靠 record_id 的内容/上下文匹配 。
record_id 格式差异(决定性线索) :
老板真实: 2a3be089-1786434502-0094199075-V2 (短uuid + unix秒 + no + -V2 )
脚本首次: 189de848-...-1786434386- (无 no、结尾 -) — 服务端解析失败
4.2 根因(决定性突破)
record_id 必须用 App 真实格式, 服务端从其中解析用户编号和时间戳来定位作业会话。
4.3 Record 模型 record_id 生成逻辑(smali 还原)
Record.smali 第 340-411 行, StringBuilder 拼接:
record_id = UUID前9位(substring 0,9) + currentTimeMillis/1000 + "-" + formatUserNo + "-V2"
即:
<uuid前9位>-<unix秒>-<formatUserNo>-V2
formatUserNo 来自 NetworkApi.getInstance().getFormatUserNo() = 0094199075(带前导0)。
改进前后对比 :
record_id
服务端解析
作业关联
错误
完整 uuid4()-秒-no-V2
no 提取失败, 结尾 -
❌ 不入库关联
正确
uuid[:9]-秒-0094199075-V2
no 正确 + V2
✅ 标 done
4.4 Record.init(BaseDrill) 字段拷贝链(smali 第4730行起)
依次调用:
setMD5 → setDrill_id → setPlaying_type → setOrientation → setPlan_id →
setCourse_id → setTraining_id → setDrill_group_id → setGroup_record_id →
setUser_course_id → setCourse_plan_id → setUser_course_plan_id →
setPlay_amounts(I) → setPlay_score(I) → setCompetition_id →
setHomework_id(J) → setPlan_day_id → setTrain_item_id
plan_day_id 只在 BaseDrill.smali 中出现(唯一模型), 由服务端反序列化填充
BaseDrill 含: homework_id, plan_day_id, plan_day_id_v2, train_item_id, competition_id, play_score, play_amounts, play_seconds, record_type, group_id, group_record_id, entrance:"homework"
4.5 达标判定(实测验证)
对每个 homework 提交一条对应 drill 的达标记录 即可让 release complete_count=1:
当天刷当天作业 → status=done, done=15/15(完美完成)
隔天补刷昨天作业 → status=delayed_done, complete_count=1(延迟完成, 同样算完成 )
4.6 delayed_done 语义澄清(关键认知修正)
重要 : delayed_done 不是反作弊/待复核标记 , 而是补做/补交作业的正常完成状态 。
判定代码实证 : App 里 u0.smali / g2.smali 的 d(String):Z(疑似 isDone)对 done 和 delayed_done 都返回 true —— 两者在 App 用户界面都显示为已完成 。
实证对比 :
release
场景
状态
complete
达标用户
368645970
8/11 当天作业当天刷
done
1
张婷婷(在 completion_patten_users)
368222937
8/10 作业隔天补刷
delayed_done
1
张婷婷(在 completion_patten_users)
结论 : 刷作业假记录确实被服务端接受并达标 ——cheating_flag=False, 用户在 completion_patten_users(达标名单)里。delayed_done 只是"隔天补做"的完成态, 与 done 同等有效。
5. 提交接口数据结构
5.1 达标记录模板 (POST /api/user/drill_record_upload)
{
"user_id": "<uid>",
"record_id": "<uuid前9位>-<unix秒>-0094199075-V2",
"drill_id": "fruit_ninja_2103",
"record_time": "2026-08-11 15:49:22",
"display_name": "切水果",
"metric_unit": "分",
"metric_type": "score",
"display_metric_unit": "分",
"display_metric_base": 1,
"success_count": 38,
"succ_count": 38,
"metric_count": 48,
"metric_amount": 48,
"score": 48.0,
"playing_type": "fruit_ninja",
"record_type": "体育作业",
"homework_id": 848795922,
"take_time": 60000,
"duration": 60000,
"begin_time": 1757xxxxxxx,
"end_time": 1757xxxxxxx,
"is_complete": 1,
"finish_type": "score",
"interactions": [ {每秒一条}
}
5.2 达标关键字段
metric_type=score, metric_amount = 要求秒数 × 5
record_type="体育作业"
homework_id = 该动作对应 homework_id
duration/take_time = 秒数 × 1000
is_complete=1, finish_type="score"
interactions: 每秒 1 条, 每条 duration≈1000ms
5.3 interactions 结构
{
"interaction_id": "i0",
"action_name": "a",
"duration": 1000,
"begin_time": 1757xxx,
"end_time": 1757xxx+1000,
"take_time": 1000,
"score": 5.0,
"succ_count": 1,
"fail_count": 0,
"error_count": 0,
"distance": 0.0,
"body": [],
"target_object": []
}
6. 自动化工具详解
6.1 工具清单(整合后)
2026-08-11 整合 : 已将原来的 ttts_batch_sweep.py / ttts_homework_auto.py / ttts_login.py 三个脚本全部并入单文件 Web 工具 ttts_sweep_web.py(登录模块已内嵌, 不再依赖外部 import)。散脚本已删除。只需一个文件即可完成全部功能 。
文件
位置
用途
ttts_sweep_web.py
~/桌面/
唯一主程序(自包含) : 全自动登录 + 账号管理 + 多账号批量刷作业
docker-compose.yml
~/桌面/
Docker 部署入口(推荐, 见末尾附章)
accounts.json
~/桌面/
账号数据(勿删, 保存所有账号 token/uid/no/设备指纹)
start_web.sh
~/桌面/
已删除(改 Docker 部署)
ttts_batch_sweep.py ttts_homework_auto.py ttts_login.py
~/桌面/
已删除(功能并入 Web)
早备份 : 渗透记录手册/逆向/天天跳绳.md
~/桌面/
完整逆向+工具文档(含登录模块源码解析)
注意 : ttts_sweep_web.py 已内置登录模块全部函数(send_sms_code / login_with_sms / _pass_slider 等), 不再需要单独的 ttts_login.py。
6.2 通用设计原则(从零写工具的思考)
工具本质就干三件事:
拉作业 : 从 details_v7 拿到当前所有未完成的 release/homework
搭记录 : 对每个 homework 构造一条真人化+达标 的提交 JSON
提交+复查 : POST drill_record_upload, 再拉一次确认 complete
所有附加功能(随机化/延时/指纹轮换/幂等)都围绕防风控 展开(详见第 7 章)。
代码组织 : 每个工具拆成"HTTP 封装 / record_id 生成 / 记录构造 / 作业获取 / 账号处理 / 主函数"六大块, 职责清晰、可独立测试。
6.3 旧版单账号 ttts_homework_auto.py(历史·逐段讲解)
① 全局配置
UID="bc4eb4e3-bd80-4043-8fd2-509412c0ff07"
NO="0094199075" # formatUserNo, 必须带前导0
HOST="https://api.tiantiantiaosheng.com"
TOKEN_FILE="/tmp/gkid_token.txt"
② 动作映射表(两个字典)
PLAYING_MAP={ # drill_id -> playing_type
"warm_up_small_step_2204":"warm_up_small_step",
"fruit_ninja_2103":"fruit_ninja", ...}
NAME_MAP={ # drill_id -> 中文名
"warm_up_small_step_2204":"小碎步", ...}
作用 : 提交记录时 playing_type 与 display_name 必须和 drill_id 对应, 否则记录异常。这类映射是从 app 动作列表 + 真实记录反推 出来的, 共 14 种动作。
③ token 加载(模块级一次性加载)
def load_token():
tok=open(TOKEN_FILE).read().strip()
if not tok: print("! token 为空, 请先从手机提取"); sys.exit(1)
return tok
TOKEN=load_token() # 模块加载时执行一次, 全局复用
④ HTTP 封装 api()
def api(url, data=None, method=None):
r=urllib.request.Request(HOST+url,
method=method or ("POST" if data is not None else "GET"))
r.add_header("Authorization","Bearer "+TOKEN)
r.add_header("x_littlelights_source","gkid")
r.add_header("X-Gkid-appVersion","4.1.14")
r.add_header("X-Gkid-deviceName","Redmi Note 5")
# ... 其余 X-Gkid 头 + UA
if data is not None:
r.add_header("Content-Type","application/json; charset=UTF-8")
r.data=json.dumps(data, ensure_ascii=False).encode()
ctx=ssl.create_default_context()
try:
return json.loads(urllib.request.urlopen(r, timeout=15, context=ctx).read())
except Exception as e:
return {"code":-1, "_err":str(e)} # 网络错误统一包装成 code:-1
设计要点 :
所有头固定拼死, 一处封装全局复用
异常捕获返回 {"code":-1} 而非抛异常, 调用处统一判 code
ensure_ascii=False 保留中文(display_name 是中文)
⑤ record_id 生成(整个项目最关键的一行 )
def make_record_id():
return f"{uuid.uuid4().hex[:9}-{int(time.time()*1000)//1000}-{NO}-V2"
uuid.uuid4().hex[:9] → 前 9 位(hex 已去连字符)
int(time.time()*1000)//1000 → unix 秒
拼 ${no} + -V2 → 与 App 完全一致
⑥ 提交一条记录 submit_record()
def submit_record(drill, hw, sec, name, playing):
now_ms=int(time.time()*1000)
dur=sec*1000; base=now_ms-dur; N=sec; score=sec*5
inter=[{...每秒一条 interaction, duration=1000, score=5.0...} for i in range(N
r={
"user_id":UID, "record_id":make_record_id(),
"drill_id":drill, "record_time":time.strftime(...),
"display_name":name, "metric_type":"score",
"metric_amount":score, "score":float(score),
"success_count":N, "succ_count":N,
"playing_type":playing, "record_type":"体育作业",
"homework_id":hw, "take_time":dur, "duration":dur,
"begin_time":base, "end_time":now_ms,
"is_complete":1, "finish_type":"score", "interactions":inter,
}
return api("/api/user/drill_record_upload", data=r)
达标核心 : metric_amount=sec*5, duration=sec*1000, 每个动作就是达标时长。
⑦ 作业获取 + 全量处理
def get_releases(days=7):
d=api(f"/api/homework/details_v7?start_date={time.strftime('%Y-%m-%d')}&days={days}")
if d.get("code")!=0: return []
return d["data"]["releases"
def done_all():
for rel in get_releases():
rid=rel.get("everyday_release_id")
hws=[h for h in rel["homeworks" if h.get("status")!="done"
if not hws: print(f"release {rid}: 已全部完成 ✓"); continue
for h in hws:
p=h["homework_proto"
drill=p["drill_id"; sec=p["each_group_count"
playing=PLAYING_MAP.get(drill) or p["playing_type"
name=NAME_MAP.get(drill) or p.get("drill_name") or drill
submit_record(drill, h["homework_id"], sec, name, playing)
# 提交完间隔2s复查一次
6.4 主力工具 ttts_batch_sweep.py(历史·详细设计)
相比旧版, 增强了 3 块: 多账号 / 真人化数据 / 完全幂等 。
① 命令行参数(argparse)
python3 ttts_batch_sweep.py # 刷全部账号
--accounts a.json # 指定账号文件 (默认 accounts.json)
--dry-run # 只看计划不提交
--no-random-order # 关闭账号顺序随机
--min-gap / --max-gap # 账号间随机休息范围 (默认 8~25s)
--account NAME # 只刷指定账号 (匹配 name 或 no)
--only-unfinished # 只刷未完成 (兼容保留)
② 设备指纹池(多账号模拟不同手机)
DEVICE_POOL=[
{"brand":"Redmi","deviceName":"Redmi Note 5","systemVersion":"11","flavor":"normal"},
{"brand":"Xiaomi","deviceName":"Mi 11","systemVersion":"12","flavor":"normal"},
{"brand":"HUAWEI","deviceName":"Nova 9","systemVersion":"12","flavor":"normal"},
{"brand":"OPPO","deviceName":"Reno 7","systemVersion":"13","flavor":"normal"},
{"brand":"vivo","deviceName":"iQOO Neo6","systemVersion":"13","flavor":"normal"},
{"brand":"Samsung","deviceName":"Galaxy A53","systemVersion":"14","flavor":"normal"},
账号配置里没写设备指纹时, 自动按 `index % 6` 轮换分配, 每个账号不同"手机"。
#### ③ 多账号 HTTP `api_req(acct, url, data)`
与旧版 `api` 的唯一区别: **头从 `acct` 字典动态取**, 而非写死全局常量:
```python
r.add_header("X-Gkid-deviceName", acct.get("deviceName","Redmi Note 5"))
r.add_header("X-Gkid-no", acct.get("no"))
r.add_header("X-Gkid-uuid", acct.get("uuid") or acct.get("uid"))
这样每个账号用自己的 token/uid/no/设备指纹。
④ 真人化记录构造 build_record()(对比旧版的核心升级)
def build_record(acct, drill, sec, name, playing):
now_ms=int(time.time()*1000)
eff_sec=max(sec, int(sec*random.uniform(1.0,1.12))) # 时长随机但不低于达标
dur=eff_sec*1000; base=now_ms-dur; N=eff_sec
metric_amount=int(sec*5*random.uniform(1.0,1.08)) # 分数≥达标, 微随机
success_count=max(1, int(N*random.uniform(0.85,0.98)))# 真人成功率<100%
inter=[]; t=base
for i in range(N):
d=random.randint(900,1100) # 每个交互0.9~1.1s
sc=round(random.uniform(3.0,5.0),1) # 分数3~5随机
inter.append({...begin_time/t/end_time 连续累计...})
t+=d
return {...} # 同旧版字段, 但所有数值已随机化
每个数值都加随机抖动 , 让每次提交数据都不完全一样, 消除机器指纹。
⑤ 完全幂等判定(核心防重刷逻辑)
def sweep_account(acct, only_unfinished):
for rel in get_releases(acct):
rid=rel.get("everyday_release_id")
hws=rel["homeworks"
rel_complete = rel.get("complete_count",0) or 0
if rel_complete>=1: # ← release 级已完成
print(f"release {rid}: 已完成 跳过 ✓"); continue
todo=[h for h in hws if h.get("status")!="done"
if not todo: print("无待提交"); continue
# ... 逐个 build_record + 提交 + sleep随机 0.8~2.2s
# 提交完 sleep 2s 再复查新状态
关键 : 用 release 级 complete_count>=1 判断(而非 homework 级 status), 因为 delayed_done 的 release homework 级永远是 uncompleted, 若按后者判断会反复刷同一套 → 风控暴露。改成 release 级判定后彻底不再重刷 。
⑥ 主函数流程
def main():
1. 解析参数
2. 加载 accounts.json
3. --account 过滤 → 只留匹配账号
4. 若未 --no-random-order: random.shuffle(accts) # 账号顺序随机
5. 为缺设备指纹的账号补指纹 (轮换池)
6. 遍历每个账号:
--dry-run → 只统计待刷数打印
否则 → sweep_account() + 账号间 random.uniform(min_gap,max_gap) 休息
7. 全自动登录模块(手机号+验证码+滑块)
用途: 新手机号 → 自动过滑块 → 发验证码 → 验证码换 token → 自动入库 → 可刷作业
注(2026-08-11 整合) : 登录模块函数已内嵌进 ttts_sweep_web.py (send_sms_code / login_with_sms / _pass_slider 等), 不再依赖单独的 ttts_login.py。本节的接口分析仍是权威参考。
7.1 登录完整链路(全自动, 已实测通过)
1. POST /api2/user/second_verification/get_coordinate_param {phone, region:"86"}
(必须手机 H5 浏览器 UA + Referer: https://www.tiantiantiaosheng.com/apph5/)
→ {code:0, data:{req_value, token}}
2. resp_value = (123*req_value + 111) % 80 + 10 ← 纯算术, 无需图像识别/模拟拖动
3. POST /api2/user/second_verification/pic_check {token, resp_value, v:null}
→ {code:0, data:{flag:true}} ← 滑块通过
4. POST /api2/user/send_login_code {phone, region} → 验证码真实下发
5. GET /api/user/sms_code_check?phone=&sms_code= → 仅校验(返回空data), 不发token
6. POST /api/user/login {phone, region, sms_code} → 真正登录, 返回 data.token(JWT)
关键 : 真正登录换 token 的是 第 6 步 POST /api/user/login (只校验不发 token / 返回空 data)。
7.2 滑块破解要点(自研算法, 无需图像)
技术栈 : 自研(无腾讯天御/极验等第三方验证码 SDK 特征)
chunk 文件 : https://www.tiantiantiaosheng.com/apph5/js/76873.12645b18.js(104061 字节)
核心算法 : resp_value = (123 * req_value + 111) % 80 + 10 —— 纯算术, 拿到 req_value 后直接算, 不需要图像识别或模拟拖动耗时
接口 : get_coordinate_param(手机号)/ get_coordinate_param_v2(账号, 实测 404 不存在)/ pic_check
7.3 「白名单」风控机制(重要)
现象 : get_coordinate_param 报 {"code":1,"msg":"该phone没有在白名单里"}
排查结论(已排除的因素) :
因素
测试
结论
App UA vs 手机浏览器 UA
用 okhttp UA 被拒, 手机 H5 UA+Referer 首次成功
UA 是必要条件 , 但非充分
多 UA 变体
换 Pixel6 Chrome119、Redmi Note5、最新 Chrome
全部被拒
冷等待
停 90~120 秒后单次纯净请求
仍被拒
请求头(Referer/Origin/Cookie)
加完整 Referer+Origin、访问页面拿 cookie
仍被拒
手机号本身
连虚构未注册号 13800000001 也被拒
非号码注册判定
当前判断 : 「白名单」是该手机号当日的滑块挑战额度/风控 ——首次冷状态放行(进白名单), 完成一次后当日移出(额度用完)。首次实测通过、之后被拒符合此规律。
影响评估 : 对刷作业无影响 。token 拿到后长期有效 , 每号只需过一次滑块拿到 token 即可一直刷。批量场景下每号一次性成本可接受。
7.4 Web 工具登录流程(新号接入)
浏览器打开 http://192.168.1.43:8088
「手机号+验证码 登录新账号」输入手机号 → 点「获取验证码」(后端自动过滑块+发码)
输入收到验证码 → 点「登录」→ 后端验证码换 token + 自动入库(去重) + 自动补 uid/no
新账号出现在「账号管理」列表, 可立即纳入「运行控制」批量刷作业
8. 风控防护设计
8.1 数据真人化(数值随机, 杜绝机器特征)
字段
随机策略
时长 eff_sec
要求秒数 + 随机0~12%(不低于达标)
分数 metric_amount
要求秒×5 × [1.0, 1.08]
成功次数 success_count
85%~98% 交互数(真人总有失误)
单交互 duration
900~1100ms 随机
单交互 score
3.0~5.0 分随机
distance
0.5~2.0 随机
时间戳 begin/end
按随机时长动态计算
8.2 请求节奏(模拟真人操作)
每个动作之间随机停 0.8~2.2 秒
账号之间随机休息 8~25 秒
账号处理顺序随机打乱
8.3 设备指纹轮换(多账号)
Redmi / Xiaomi / HUAWEI / OPPO / vivo / Samsung 自动轮换
每个账号用不同 deviceName / systemVersion / brand
8.4 幂等 + 安全
release 级 complete_count>=1 即跳过 (含 delayed_done) — 完全幂等, 不重复提交
失败自动重试 + 全局节流
--dry-run 安全演练
账号异常 try/except 隔离, 单个失败不影响其他账号
9. 工具开发迭代与踩坑
9.1 开发演进时间线(工具如何一步步成型)
v0 手动验证 (gkid_details_old.py / gkid_submit_all.py): 逐个调接口试参数, 确认哪条能 complete
v1 首次打通 : 发现 record_id 格式问题, 用正确格式提交后 release 368645970 → done 15/15(关键里程碑)
v2 批量化 : 写 ttts_homework_auto.py 自动拉取+批量提交, 第二 release 补刷 → delayed_done
v3 多账号+风控 : 写 ttts_batch_sweep.py, 加多账号/设备指纹/真人化随机
v4 彻底幂等 : 发现 delayed_done 的 release 被反复刷, 改用 release 级 complete 判定 → 完全跳过
9.2 踩坑实录(含代码级 bug 修复)
record_id 用错格式 → 记录入库但不关联作业。修复: 改成 App 真实格式。这是唯一一个"改了配方立刻成功"的修复。
token 截断导致 41001 → 误判"账号被风控"。修复: 重新完整提取 token。教训:先验证 token 是否完整再怀疑风控。
delayed_done 被反复刷 : v3 用 homework 级 status 判断, delayed_done 的 release homework 级永为 uncompleted → 每次跑都重刷。修复: 改用 complete_count>=1 release 级判定。
hws 变量未定义 bug : 编辑删掉了 hws=rel["homeworks"] 但下方仍引用 → NameError。修复: 恢复赋值并做完整验证。
shell heredoc parse error : exec 内嵌套 heredoc(含 >/$/引号) 触发 parse error near ')'。规避:一律写独立 .py 文件用 python3 执行。
Web 工具前端 lg_slider 残留引用 : 改全自动时删了手动滑块 UI 元素, 但 JS 函数里仍引用 $('lg_slider')/$('lg_slider_url') → JS 运行时访问 null 抛异常, 登录流程中断。修复: 删除 $('lg_slider') 相关代码。教训: 删 UI 元素时同步清理 JS 引用 。
前端手机号/验证码正则 \\d 双反斜杠 : Python 三引号字符串里写的 \\d 到 JS 正则变成字面反斜杠, /^1\\d{10}$/ 永远匹配不到数字 → 手机号校验恒失败, 无法登录。这是真 bug 。修复: 改单反斜杠 \d。教训: JS 嵌 Python 三引号, 反斜杠转义层叠, 写完后 node 模拟执行全流程验证正则 。
9.3 通用踩坑(环境层面)
frida 对强加固 App 不可用 : vDexVMP/InnoSecure 检测注入即 art 崩溃, 别浪费精力, 转纯静态。
$PATH 陷阱 : exec 需先 export PATH=/usr/bin:/bin:/usr/local/bin:/usr/sbin:/sbin; frida/objection 在 ~/.local/bin 需额外 export。
adb 变量陷阱 : adb -s 48c0f066 需完整路径, $ADB 赋值会在新 shell 丢失。
Kali Python 是管理式环境(PEP 668) : pip 装 frida/objection 需 --break-system-packages。
frida-server 版本必须与 PC frida python 完全一致 (当前 17.17.0)。
Python re.search 转义 : 独立 .py 中 re.search('"'+key+r'":\s*"?', s) 建议用 raw string 避免 SyntaxWarning: invalid escape sequence '\s'。
10. 附录: 完整接口清单
10.1 用户/记录
GET /api/user/drill_record_list_v5?page=&page_size=
POST /api/user/drill_record_upload
GET /api/user/today_homework_share?everyday_release_id=
10.2 作业 (HomeworkService)
接口
用途
GET /api/homework/details_v7
作业列表(精简)
GET /api/homework/details
老版详情(含 BaseDrill 完整字段)
GET /api/homework/head
作业头部
POST /api/homework/update_master
更新作业
GET /api2/homework/ai_teacher_info
AI 老师信息
GET /api2/homework/get_remind_info
提醒信息
POST /api2/homework/set_remind_time
设置提醒时间
GET /api/school/message
学校消息
10.3 课程/游玩
POST /api/course/drill_playing (仅 drill_name)
POST /api/course/course_plan_playing (返回 CoursePlayingRsp)
10.4 返回 BaseDrill 的接口(含 plan_day_id/train_item_id 等关联字段)
UserService、GroupChallengeRsp$DataBean、ChallengeDataRsp$ DataBean、InstitutionService、SmallSortDrillRsp$DataBean、PlayDrillRsp、PlanPlayRsp$ DataBean$DrillGroupBean、**HomeworkDetailRsp$ DataBean**、DrillListRsp、HomeServiceV2
附: 本次成果数据
release 368645970 (8/11 布置, 15 个作业): status=done, done=15/15, complete_count=1
release 368222937 (8/10 布置, 15 个作业): status=delayed_done, complete_count=1
均由老师"朝日格图"布置, 学生"张婷婷"名下显示完成
全自动登录链(滑块+验证码)已实现并首次实测通过 , 当前因该手机号当日滑块额度用完被拒, 等次日/换新号即可全自动验证
Web 工具最新版已修复前端 lg_slider 残留 + 手机号/验证码正则 bug, 运行于 http://192.168.1.43:8088
唯一主程序 : /home/leonzane/桌面/ttts_sweep_web.py(自包含单文件: 登录+账号+刷作业)
部署方式 : Docker 极简版(桌面 3 文件 + 一个 yaml), 见文档末尾「附: Docker 部署」
动作映射表覆盖 14 种动作
附: Docker 部署(极简, 推荐)
2026-08-11 : 已 Docker 化并精简。桌面只保留 3 个文件, 部署只需一个 compose + 一条命令。
所需文件(桌面根目录)
~/桌面/
├── docker-compose.yml # 部署入口(唯一 yaml)
├── ttts_sweep_web.py # 主程序(yaml 挂载, 勿删)
└── accounts.json # 账号数据(持久化, 勿删)
启动
cd /home/leonzane/桌面
# ⚠️ 桌面是中文目录名, compose 无法自动推项目名, 必须显式 -p
docker compose -p ttts up -d
# 浏览器: http://127.0.0.1:8088 (本机) / http://192.168.1.43:8088 (局域网)
常用命令
docker compose -p ttts up -d # 启动
docker compose -p ttts down # 停止(数据在宿主机, 不丢)
docker compose -p ttts restart # 重启
docker compose -p ttts logs -f # 看日志
docker compose -p ttts ps # 看状态(应 Up healthy)
设计要点
用官方 python:3.12-slim 镜像 , bind mount 挂载主程序+数据, 启动时自动 pip install flask。无需自定义镜像/Dockerfile/entrypoint 。
数据持久化 : accounts.json 在宿主机, 容器重建/升级/重启账号不丢; 备份只需拷贝这一个文件。
健康检查 走 /api/status, 配 restart: unless-stopped 异常自动拉起。
时区已设 TZ=Asia/Shanghai。
`