diff --git a/findings.md b/findings.md index cacb7f2..29148b9 100644 --- a/findings.md +++ b/findings.md @@ -476,6 +476,31 @@ - 端到端验证后 19+291 测试回归无新增失败。诊断体系(JSONL 探针/ strE notes)保留, 供后续问题定位。 +## 2026-08-27 - HAR 完整度审计: 补漏上报 + seed/counter 细节 + +- 逐请求对比 07_10 HAR 的真机广告周期发现缺口(金币变少/风控画像来源): + 1. **`/rest/r/ad/exposure/report`(ad_exit 完成事件)缺失** —— 真机在 + task/report 前数百毫秒发出, bizStr 为明文 JSON(pageId/subPageId/ + llsid/creativeId/uid/taskId/eventType/sessionId/materialDuration/ + viewDuration/isCompleted/items), 全字段可从素材推导。已实现并在 + 线验证: exposure result=1 + task report result=1(+611 金币)。 + 2. **adlog 播放事件流缺失**(photo/action, 每次观看 15~25 条): `log` + 参数为自定义 "gzip2" 二进制编码, base64 后非 gzip/zlib, 项目无 + 既有逆向 —— 短期不可复现, 已用 exposure ad_exit 作主要完成信号, + 脚本 TODO 标注。这是剩余最大的播放信号缺口。 + 3. 页面加载 H5 调用补齐: signIn/resource + retain/show + todo/tasks + (真机赚钱页每次加载必发)。 + 4. misc2/ulog 埋点未发送(show_event 任务曝光流等), 已知缺口未实现。 +- seed/counter 细节审查结论: + - 10418 seed=srand(启动秒);rand()+1 ✓(findings 已有 1785049292→ + 0x6f5d0faa 精确复现记录); counter 起步 0x5F+1 ✓(真机首见 ~0x60)。 + - **修正**: 真机进程内 fetch/exposure/report 之间还有日志等原生请求 + 推进共享 counter(连续 sig3 有间隙); 我们原实现严格 +1 是机器特征, + 已改为每周期结束推进 2~6 随机间隙。 + - H5 sig3 counter(独立 envelope)按抓包递增 ✓; elapsed=(uptime<<16)|ms、 + state=0x10000|16bit、seed 每会话随机, 均按抓包字段格式。 +- 在线端到端再验证: fetch→观看→exposure(result=1)→report(+611 金币)。 + --- *每执行2次查看/浏览器/搜索操作后更新此文件* *防止视觉信息丢失* diff --git a/ksjsb.py b/ksjsb.py index 60bfa79..7c3c044 100644 --- a/ksjsb.py +++ b/ksjsb.py @@ -36,8 +36,12 @@ T1/T2 密码表 base64 内嵌(提取自 libkwsgmain.so) TODO(抓包无法覆盖): - - 广告「打开详情/落地页」属第三方广告 SDK 行为, 不在脚本范围; - 奖励由 ad/task/report 直接发放。 + - adlog 播放事件流(POST adlog.e.kuaishou.com/rest/nebula/log/ad/photo/action, + 真机每次观看发 15~25 条, `log` 为自定义 "gzip2" 二进制编码, 未逆向): + 这是最大的缺失播放信号; 已用 exposure/report(ad_exit, 明文 bizStr) + 补完成事件作为主要替代。 + - misc2 埋点(任务曝光/点击 show_event 流)未发送; ulog 加密日志未发送。 + - 广告「打开详情/落地页」属第三方广告 SDK 行为, 不在脚本范围。 - API 请求用 requests(HTTP/1.1); 真机为 OkHttp/Cronet(H2/QUIC), 传输指纹差异已知, 未在本脚本处理。 @@ -5197,11 +5201,23 @@ def signed_api_url( REWARD_SDK = "95147564-9763-4413-a937-6f0e3c12caf1" APP_NAME = "快手极速版" -AD_FETCH_FALLBACK_BUSINESS = {"sign_in": 605, "treasure": 606, "normal": 672} +AD_FETCH_FALLBACK_BUSINESS = {"sign_in": 605, "treasure": 606, "normal": 672, "goods": 7038} AD_FETCH_FALLBACK_SCENE = { "sign_in": (11101, 100024058, 20342), "treasure": (11101, 100024064, 20346), "normal": (11101, 100026367, 24067), + "goods": (11101, 100074584, 96134), +} +# 高频任务的任务位 materialKey 前缀 —— 2026-08-27 抓包 + 实况任务池 +# (任务按账号/日期轮换, 前缀集合覆盖已观察到的全部变体) +HF_TASK_PREFIXES = { + "TASK_LIST_20048_": "走路赚金币", + "TASK_LIST_60151_": "搜好货看广告", + "TASK_LIST_60006_": "搜索看广告", + "TASK_LIST_176_": "搜索赚金币", + "TASK_LIST_148_": "逛街领金币", + "TASK_LIST_60230_": "搜短剧看广告", + "TASK_LIST_20505_": "刷广告视频赚金币", } @@ -5832,6 +5848,9 @@ class KsJsB: self.ad_task_countdown_ms: dict[str, int] = {} self.task_ad_slot: dict[str, Any] | None = None self.task_treasure_slot: dict[str, Any] | None = None + self.hf_task_slots: dict[str, dict[str, Any]] = {} # 前缀 -> 任务位(走路/搜好货/搜索等) + self.dsp_tasks: list[dict[str, Any]] = [] # extParam 带 reportUrl 的外部广告任务 + self.goods_task_name: str = "搜好货看广告" # 泛化后实际命中的任务名 self.last_task_list_json: Any = None # 点击上报为运营分析事件, 非必需(宝箱开箱已验证无点击也可领奖); # 首次失败后停发, 避免重复触发风控 @@ -6094,6 +6113,16 @@ class KsJsB: def treasure_box_info(self) -> Record: return self._h5_get("treasure_info", "宝箱信息", "/rest/wd/encourage/unionTask/treasureBox/info", {"source": SOURCE}) + def sign_in_resource(self) -> Record: + """真机赚钱页每次加载必发(2026-08-27 抓包)。""" + return self._h5_get("sign_in_resource", "签到资源", "/rest/wd/encourage/unionTask/signIn/resource", {}) + + def retain_show(self) -> Record: + return self._h5_get("retain_show", "留存弹窗", "/rest/wd/encourage/unionTask/retain/show", {"refresh": "false"}) + + def todo_tasks(self) -> Record: + return self._h5_get("todo_tasks", "待办任务", "/rest/n/nebula/activity/earn/todo/tasks", {}) + def treasure_box_report(self, tracking: dict[str, Any]) -> Record: body_text = compact_json({ "imei": "", @@ -6233,7 +6262,7 @@ class KsJsB: headers["Cookie"] = self._api_cookie_header() record = self._request( f"{kind}_ad_fetch", - {"sign_in": "签到广告拉取", "treasure": "宝箱广告拉取", "normal": "看广告得金币拉取"}[kind], + {"sign_in": "签到广告拉取", "treasure": "宝箱广告拉取", "normal": "看广告得金币拉取", "goods": "搜好货广告拉取"}[kind], "POST", url, headers, urllib.parse.urlencode(body_pairs).encode("utf-8"), notes={ "strE": str_e.decode("utf-8", "replace"), @@ -6251,7 +6280,7 @@ class KsJsB: material = self.ad_materials.get(kind) if material is None: material = fallback_ad_material(kind) if self.dry_run else None - title = {"sign_in": "签到广告上报", "treasure": "宝箱广告上报", "normal": "看广告得金币上报"}[kind] + title = {"sign_in": "签到广告上报", "treasure": "宝箱广告上报", "normal": "看广告得金币上报", "goods": "搜好货广告上报"}[kind] if material is None: record = Record(f"{kind}_ad_report", title) record.error = "missing_ad_material" @@ -6265,6 +6294,44 @@ class KsJsB: headers["Cookie"] = self._api_cookie_header() return self._request(f"{kind}_ad_report", title, "POST", url, headers, urllib.parse.urlencode(body_pairs).encode("utf-8")) + def exposure_report(self, kind: str) -> Record: + """广告 ad_exit 完成事件(真机在 task/report 前数百毫秒发出, 明文 bizStr)。 + + 风控用观看完成信号评估奖励质量; 缺失会压低广告质量分。字段全部 + 可从素材推导(2026-07-10 抓包实证)。 + """ + material = self.ad_materials.get(kind) + if material is None: + material = fallback_ad_material(kind) if self.dry_run else None + if material is None: + record = Record(f"{kind}_ad_exposure", "广告完成上报") + record.error = "missing_ad_material" + return record + uid = self.cookie.get("userId") or self.cookie.get("ud") or "" + duration_s = max(1, material.material_time // 1000) + session_id = material.session_id or f"adNeo-{uid}-{material.sub_page_id}-{int(time.time() * 1000)}" + biz = { + "pageId": material.page_id, + "subPageId": material.sub_page_id, + "llsid": str(material.llsid or "0"), + "creativeId": _safe_int(material.creative_id), + "uid": uid, + "taskId": material.business_id, + "eventType": "ad_exit", + "sessionId": session_id, + "materialDuration": duration_s, + "isInflow": False, + "viewDuration": duration_s, + "isCompleted": True, + "extMap": {}, + "items": [], + } + body_pairs = [("bizStr", compact_json(biz))] + self._api_body_common() + url = signed_api_url("/rest/r/ad/exposure/report", self._api_params(), body_pairs, self.client_salt, self._resolve_api_state()) + headers = dict(API_HEADERS) + headers["Cookie"] = self._api_cookie_header() + return self._request(f"{kind}_ad_exposure", "广告完成上报", "POST", url, headers, urllib.parse.urlencode(body_pairs).encode("utf-8")) + def _update_ad_contexts(self, data: Any) -> None: if not isinstance(data, dict): return @@ -6297,21 +6364,48 @@ class KsJsB: ext_param = task.get("extParam") if isinstance(ext_param, dict) and kind == "normal": self.ad_task_countdown_ms[kind] = max(0, _safe_int(ext_param.get("descCountDown"), 0)) - # 任务位提取(看广告任务 + 宝箱挂件) + # 任务位提取(看广告任务/宝箱挂件/高频任务/外部广告任务) for task in tasks: if not isinstance(task, dict): continue + ext_param = task.get("extParam") + ext_param = ext_param if isinstance(ext_param, dict) else {} tracking = task.get("eventTrackingLogInfo") if not isinstance(tracking, dict): tracking = dig(task, "extParam.eventTrackingLogInfo") if not isinstance(tracking, dict) or not tracking: - continue + tracking = {} material_key = str(tracking.get("materialKey") or "") resource_id = str(tracking.get("resourceId") or "") if material_key.startswith("TASK_LIST_17_") and self.task_ad_slot is None: self.task_ad_slot = tracking if resource_id == "externalFeed_externalTreasureChestWidget" and self.task_treasure_slot is None: self.task_treasure_slot = tracking + for prefix in HF_TASK_PREFIXES: + if material_key.startswith(prefix): + self.hf_task_slots.setdefault(prefix, tracking) + # "搜好货类"看广告任务泛化: linkType=101 且 linkUrl 解码为 + # matrixMixedAd 场景(pageId=11101, 非基础 605/606/672 的 businessId, + # 如搜好货 7038 / 搜短剧 7052) -> 接入原生 reward 链 kind=goods + decoded = decode_neo_params(str(task.get("linkUrl") or "")) + if ( + _safe_int(task.get("linkType")) == 101 + and _safe_int(decoded.get("pageId")) == 11101 + and _safe_int(decoded.get("businessId")) not in (0, 605, 606, 672) + ): + self.ad_fetch_neo_params["goods"] = str(task.get("linkUrl") or "") + self.ad_fetch_scenes["goods"] = ( + _safe_int(decoded.get("pageId"), AD_FETCH_FALLBACK_SCENE["goods"][0]), + _safe_int(decoded.get("subPageId"), AD_FETCH_FALLBACK_SCENE["goods"][1]), + _safe_int(decoded.get("posId"), AD_FETCH_FALLBACK_SCENE["goods"][2]), + ) + self.ad_fetch_business["goods"] = _safe_int(decoded.get("businessId"), AD_FETCH_FALLBACK_BUSINESS["goods"]) + self.goods_task_name = str(task.get("name") or "搜好货看广告") + # 外部广告任务(extParam 带 reportUrl, 如"打开千问必得金币"): 全链客户端可复现 + if ext_param.get("reportUrl") and tracking: + entry = {"tracking": tracking, "task": task, "ext": ext_param} + if not any(d["tracking"].get("eventTrackingTaskId") == tracking.get("eventTrackingTaskId") for d in self.dsp_tasks): + self.dsp_tasks.append(entry) # ---------------- 任务步骤 ---------------- def _best_effort_click(self, slot: dict[str, Any] | None, *, threshold: bool = False) -> None: @@ -6445,6 +6539,8 @@ class KsJsB: if wait_seconds > 0: print(f"[看广告] 等待广告模拟观看 {wait_seconds:.1f}s ...") time.sleep(wait_seconds) + # 完成事件 -> 任务上报(真机顺序: exposure ad_exit 先于 task/report) + self._print(self.exposure_report("normal")) report = self.report_ad("normal") self._print(report) if not report.ok: @@ -6452,6 +6548,10 @@ class KsJsB: break done += 1 print(f"[看广告] 第 {done}/{planned} 次完成") + # 真机进程内还有日志等原生请求推进共享 10418 counter, + # 我们的连续 API 请求之间补 2~6 的随机间隙避免严格 +1 的机器特征 + state = self._resolve_api_state() + state.counter = (state.counter + random.randint(2, 6)) & 0xFFFFFFFF if not self.dry_run and done < planned: time.sleep(random.uniform(2.0, 6.0)) if done % 5 == 0: @@ -6459,6 +6559,113 @@ class KsJsB: print(f"[看广告] 完成 {done}/{planned} 次") return done + # ---------------- 高频任务(走路/搜好货/搜索) + 外部广告任务 ---------------- + @staticmethod + def build_dsp_body(entry: dict[str, Any]) -> dict[str, Any]: + """外部广告 dsp/report body: 全部来自任务列表下发(2026-08-27 抓包映射)。 + + reqId/impId/trackers 直拷 extParam; admInfo 客户端拼装 + (title=任务名, subTitle=任务 desc, dpLink=link.linkUrl, + h5Link=extParam.downloadUrl); reportType=1。 + """ + task, ext = entry["task"], entry["ext"] + adm_info = { + "adId": ext.get("adId"), + "title": task.get("name"), + "subTitle": task.get("desc"), + "dpLink": dig(task, "link.linkUrl") or "", + "h5Link": ext.get("downloadUrl") or "", + "appIdentifier": ext.get("appIdentifier") or "", + } + return { + "reqId": ext.get("reqId"), + "bidId": ext.get("bidId"), + "impId": ext.get("impId"), + "admInfo": adm_info, + "adBidType": ext.get("adBidType"), + "dspProvider": ext.get("dspProvider"), + "reportType": 1, + "downEndTrackersUrl": ext.get("downEndTrackersUrl"), + "dplkTrackersUrl": ext.get("dplkTrackersUrl"), + "implTrackersUrl": ext.get("implTrackersUrl"), + } + + def dsp_report(self, entry: dict[str, Any]) -> Record: + """外部广告曝光上报(POST 明文 JSON); 奖励需广告侧 S2S 激活回调。""" + body_text = compact_json(self.build_dsp_body(entry)) + url = f"{NEBULA_BASE}{entry['ext'].get('reportUrl') or '/rest/n/nebula/exchange/dsp/report'}?{urllib.parse.urlencode({'__NS_sig3': self._h5_sig3(body=body_text, method='POST'), 'sigCatVer': '1'})}" + headers = self._h5_headers(method="POST", url=url, body=body_text, json_body=True) + name = str(entry["task"].get("name") or "外部广告")[:12] + return self._request("dsp_report", f"外部广告曝光上报({name})", "POST", url, headers, body_text.encode("utf-8")) + + def hf_tasks_step(self) -> None: + """走路/搜好货/搜索任务位点击 + 外部广告(dsp)任务曝光上报。 + + 依据 2026-08-27 三份抓包: 这三类任务的奖励链主体在原生侧 + (步数/搜索行为/外部 App 激活 S2S), 客户端可见动作为任务位点击 + 与 dsp 曝光上报, 此处如实复现。 + """ + if not (self.hf_task_slots or self.dsp_tasks): + print("[高频任务] 任务列表未发现走路/搜好货/搜索/外部广告任务") + return + for prefix, name in HF_TASK_PREFIXES.items(): + slot = self.hf_task_slots.get(prefix) + if slot is None: + continue + print(f"[高频任务] {name}: 点击任务位") + self._best_effort_click(slot) + for entry in self.dsp_tasks: + task = entry["task"] + print(f"[外部广告] {task.get('name')}: 曝光上报") + self._print(self.dsp_report(entry)) + + def goods_ad_step(self, *, ad_wait: float, count: int) -> int: + """搜好货看广告(原生 reward 链, businessId 7038, scene 取任务 linkUrl)。 + + 抓包中该任务完成度经搜索行为推进; 纯广告观看是否计次由服务端 + 判定, 失败即止不强刷。 + """ + if self.dry_run: + # dry-run 下没有任务列表上下文, 用 fallback scene 演示一次 + fetch = self.fetch_ad("goods") + self._print(fetch) + if fetch.ok: + self.ad_materials["goods"] = fallback_ad_material("goods") + self._print(self.exposure_report("goods")) + self._print(self.report_ad("goods")) + return 1 + if "goods" not in self.ad_fetch_scenes and "goods" not in self.ad_fetch_business: + print(f"[{self.goods_task_name}] 任务列表未提供该类广告场景, 跳过") + return 0 + done = 0 + for i in range(max(1, count)): + fetch = self.fetch_ad("goods") + self._print(fetch) + if not fetch.ok: + print("[搜好货] 拉取失败, 提前结束") + break + material = self.ad_materials.get("goods") + if material is None: + break + wait_seconds = (material.material_time / 1000.0) if ad_wait < 0 else max(0.0, ad_wait) + if wait_seconds > 0: + print(f"[{self.goods_task_name}] 等待广告模拟观看 {wait_seconds:.1f}s ...") + time.sleep(wait_seconds) + self._print(self.exposure_report("goods")) + report = self.report_ad("goods") + self._print(report) + if not report.ok: + print(f"[{self.goods_task_name}] 上报失败, 提前结束") + break + done += 1 + print(f"[{self.goods_task_name}] 第 {done} 次完成") + state = self._resolve_api_state() + state.counter = (state.counter + random.randint(2, 6)) & 0xFFFFFFFF + if i + 1 < count: + time.sleep(random.uniform(2.0, 6.0)) + print(f"[{self.goods_task_name}] 完成 {done} 次") + return done + def _new_uuid4() -> str: import uuid @@ -6487,25 +6694,39 @@ def run() -> int: print(f"===== 快手极速版每日任务{' (dry-run)' if dry_run else ''} =====") if client.record_path is not None: print(f"诊断日志: {client.record_path}") - print("--- 1/5 查询金币/余额 ---") + print("--- 1/7 查询金币/余额 ---") client._print(client.basic_info()) - print("--- 2/5 任务列表(刷新广告上下文) ---") + print("--- 2/7 任务列表(刷新广告上下文) ---") client._print(client.task_list()) + # 真机赚钱页每次加载必发的其余 H5 调用(2026-08-27 抓包), + # 保持页面加载行为完整性 + client._print(client.sign_in_resource()) + client._print(client.retain_show()) + client._print(client.todo_tasks()) - print("--- 3/5 签到 ---") + print("--- 3/7 签到 ---") client.sign_in_step() - print("--- 4/5 开宝箱 ---") + print("--- 4/7 开宝箱 ---") client.treasure_box_step() - print("--- 5/5 看广告得金币 ---") + print("--- 5/7 高频任务(走路/搜好货/搜索/外部广告) ---") + client.hf_tasks_step() + + print("--- 6/7 看广告得金币 ---") client.watch_ads_step( ad_wait=float(os.environ.get("KS_AD_WAIT_SECONDS", "-1")), low=int(os.environ.get("KS_AD_COUNT_MIN", "20")), high=int(os.environ.get("KS_AD_COUNT_MAX", "30")), ) + print(f"--- 7/7 {client.goods_task_name} ---") + client.goods_ad_step( + ad_wait=float(os.environ.get("KS_AD_WAIT_SECONDS", "-1")), + count=int(os.environ.get("KS_GOODS_AD_COUNT", "1")), + ) + print("--- 最终余额 ---") client._print(client.basic_info()) finally: diff --git a/tests/test_ksjsb.py b/tests/test_ksjsb.py index e89e676..a68cd80 100644 --- a/tests/test_ksjsb.py +++ b/tests/test_ksjsb.py @@ -332,6 +332,89 @@ class TaskSlotTests(unittest.TestCase): self.assertEqual(client.ad_fetch_scenes["normal"], (11101, 100026367, 24067)) self.assertEqual(client.ad_task_countdown_ms["normal"], 30000) + def test_update_ad_contexts_extracts_hf_and_dsp_tasks(self): + import base64 as b64 + + goods_link = b64.b64encode( + compact_json({"pageId": 11101, "subPageId": 100074584, "posId": 96134, "businessId": 7038}).encode() + ).decode() + drama_link = b64.b64encode( + compact_json({"pageId": 11101, "subPageId": 100134702, "posId": 175158, "businessId": 7052}).encode() + ).decode() + walk_slot = {"materialKey": "TASK_LIST_20048_WALKING_NOT_REWARDED", "eventTrackingTaskId": 20048} + goods_slot = {"materialKey": "TASK_LIST_60151_PROGRESS", "eventTrackingTaskId": 60151} + search_slot = {"materialKey": "TASK_LIST_176_ACTIVE_PASSIVE_TASK_UNFINISH", "eventTrackingTaskId": 176} + dsp_slot = {"materialKey": "60494_TASK_LIST_EXCHANGE_DSP_1", "eventTrackingTaskId": 60494} + dsp_task = { + "name": "打开千问必得金币", + "desc": "千问智能解答,高效省时间", + "link": {"linkUrl": "tongyi://deeplink?x=1"}, + "eventTrackingLogInfo": dsp_slot, + "extParam": { + "reportUrl": "/rest/n/nebula/exchange/dsp/report", + "adId": "2162", "appIdentifier": "com.aliyun.tongyi", + "downloadUrl": "https://aisite.wejianzhan.com/site/x", + "reqId": "dsctREQ", "impId": "imp-1", "bidId": None, + "adBidType": 1, "dspProvider": "happygame", + "implTrackersUrl": ["http://t/1"], "downEndTrackersUrl": None, "dplkTrackersUrl": None, + }, + } + client = make_client() + client._update_ad_contexts({ + "data": { + "dailyTasks": [ + {"eventTrackingLogInfo": walk_slot}, + # linkType=101 + pageId=11101 + businessId 7038 -> goods 场景(搜好货) + {"eventTrackingLogInfo": goods_slot, "linkUrl": goods_link, "linkType": 101, "name": "搜好货看广告"}, + # 后出现的同形态任务(搜短剧 7052)覆盖 goods 场景(取最新) + {"eventTrackingLogInfo": search_slot, "linkType": 101, "linkUrl": drama_link, "name": "搜短剧看广告"}, + dsp_task, + ] + } + }) + self.assertEqual(client.hf_task_slots["TASK_LIST_20048_"], walk_slot) + self.assertEqual(client.hf_task_slots["TASK_LIST_60151_"], goods_slot) + self.assertEqual(client.hf_task_slots["TASK_LIST_176_"], search_slot) + self.assertEqual(client.ad_fetch_scenes["goods"], (11101, 100134702, 175158)) + self.assertEqual(client.ad_fetch_business["goods"], 7052) + self.assertEqual(client.goods_task_name, "搜短剧看广告") + self.assertEqual(len(client.dsp_tasks), 1) + self.assertEqual(client.dsp_tasks[0]["task"], dsp_task) + + def test_dsp_body_assembly(self): + """admInfo 拼装规则与 2026-08-27 抓包映射一致。""" + entry = { + "task": { + "name": "打开千问必得金币", + "desc": "千问智能解答,高效省时间", + "link": {"linkUrl": "tongyi://www.tongyi.cn/x?src_pkg=zslytylhpl"}, + }, + "ext": { + "adId": "2162", "appIdentifier": "com.aliyun.tongyi", + "downloadUrl": "https://aisite.wejianzhan.com/site/y", + "reqId": "dsctREQ", "impId": "imp-1", "bidId": None, + "adBidType": 1, "dspProvider": "happygame", + "implTrackersUrl": ["http://t/1"], "downEndTrackersUrl": None, "dplkTrackersUrl": None, + }, + } + body = KsJsB.build_dsp_body(entry) + self.assertEqual( + list(body.keys()), + ["reqId", "bidId", "impId", "admInfo", "adBidType", "dspProvider", "reportType", "downEndTrackersUrl", "dplkTrackersUrl", "implTrackersUrl"], + ) + self.assertEqual(body["reportType"], 1) + self.assertEqual( + body["admInfo"], + { + "adId": "2162", + "title": "打开千问必得金币", + "subTitle": "千问智能解答,高效省时间", + "dpLink": "tongyi://www.tongyi.cn/x?src_pkg=zslytylhpl", + "h5Link": "https://aisite.wejianzhan.com/site/y", + "appIdentifier": "com.aliyun.tongyi", + }, + ) + class SequentialRunTests(unittest.TestCase): def test_run_dry_run_completes_all_steps_without_network(self): @@ -352,11 +435,16 @@ class SequentialRunTests(unittest.TestCase): os.environ[k] = v self.assertEqual(code, 0) text = buf.getvalue() - for marker in ("1/5 查询金币/余额", "3/5 签到", "4/5 开宝箱", "5/5 看广告得金币", "最终余额", "任务执行完成"): + for marker in ( + "1/7 查询金币/余额", "3/7 签到", "4/7 开宝箱", + "5/7 高频任务", "6/7 看广告得金币", "7/7 搜好货看广告", # dry-run 下无任务列表, 标签为默认名 + "最终余额", "任务执行完成", + ): self.assertIn(marker, text) self.assertIn("开宝箱 dry-run method=POST", text) self.assertIn("/rest/e/reward/mixed/ad", text) self.assertIn("/rest/r/ad/task/report", text) + self.assertIn("搜好货广告拉取", text) if __name__ == "__main__":