feat: 接入云手机积分签到、任务与夏日刮一刮

在 cu.py 增加 uphone_task:SSO 登录后完成积分签到与可 API 任务领取,余额达阈值则十连,夏日活动每日仅领一次抽奖次数并抽奖。同步补充 README 模块说明。
This commit is contained in:
yule153604 committed 2026-07-10 16:57:24 +08:00
1 parent acb04034de
commit a44a8d4ec5
2 files changed
+418 -1

No files matched your search

+13 -1
View File
@@ -29,6 +29,17 @@ cu.py
| 云盘乘风活动 | `cloudpan_task` | 需云盘 token |
| 上传大比拼 | `cloud_battle_task` | 需云盘 token |
| 商都福利 | `shangdu_task` | 按归属地跳过 |
| 云手机积分 | `uphone_task` | 见下方说明 |
### 云手机积分(`uphone_task`)
在手厅 `ecs_token` 基础上换票进入云手机活动体系,顺序为:
1. **SSO**:`getTicketByNative` → `getTokenByTicket` → 活动 `user/login`
2. **积分签到**(`Points_Sign_2507`)
3. **赚积分任务**(`Points_Obtain_2507`):可 API 完成的任务自动上报并领取;讨论区、看广告等需真机行为的任务跳过
4. **积分十连**:余额 ≥ 阈值且当日仍有次数时执行(默认满 100 分)
5. **夏日刮一刮**(`HD2026062200218`):每日只领取 **1 次**抽奖机会(优先「去赚积分」任务),有次数则抽完
## 快速开始
@@ -67,6 +78,7 @@ python cu.py
| `UNICOM_CLOUD_BATTLE_FILE` | `文本.txt` | 上传文件名 |
| `UNICOM_CLOUD_BATTLE_CONTENT` | `1` | 上传文件内容 |
| `SHANGDU_LOTTERY_MAX` | 剩余次数 | 单次最多抽奖次数 |
| `UNICOM_UPHONE_LOTTERY_COST` | `100` | 云手机积分十连触发余额阈值 |
## 说明
@@ -76,4 +88,4 @@ python cu.py
## 免责声明
本项目仅供学习交流,不得用于违法用途。开发者不对使用后果承担责任。
本项目仅供学习交流,不得用于违法用途。开发者不对使用后果承担责任。
+405
View File
@@ -121,6 +121,28 @@ SHANGDU_LOTTERY_PRIZES = [
SHANGDU_LOTTERY_FESTIVAL_PRIZES = ["30元话费券", *SHANGDU_LOTTERY_PRIZES]
SHANGDU_LOTTERY_MAX = read_int_env("SHANGDU_LOTTERY_MAX", default=None, minimum=0)
# 云手机积分 / 夏日刮一刮
UPHONE_H5API = "https://uphone.wostore.cn/h5api"
UPHONE_WO_ADV = "https://uphone.wo-adv.cn"
UPHONE_CHANNEL = "ST-Wode"
UPHONE_CHANNEL_H5 = "ST-Jingang002"
UPHONE_EDOP_APP_ID = "edop_unicom_68e8fa69"
UPHONE_ACT_SIGN = "Points_Sign_2507"
UPHONE_ACT_OBTAIN = "Points_Obtain_2507"
UPHONE_ACT_EXCHANGE = "Points_Exchange_2507"
UPHONE_ACT_SUMMER = "HD2026062200218"
UPHONE_GOODS_LOTTERY_10 = "2026031010"
UPHONE_LOTTERY_COST = read_int_env("UNICOM_UPHONE_LOTTERY_COST", default=100, minimum=1)
UPHONE_OBTAIN_SKIP = frozenset({"012-4", "TASK2026010601"})
UPHONE_APP_UA = (
"ChinaUnicom4.x/12.13 (com.chinaunicom.mobilebusiness; build:1; iOS 27.0.0) "
"Alamofire/4.7.3 unicom{version:iphone_c@12.1300}"
)
UPHONE_H5_UA = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 "
"(KHTML, like Gecko) unicom{version:iphone_c@12.1300};ltst;OSVersion/27.0"
)
def require_int(value, field, minimum=None):
"""必填业务整数:仅接受非 bool 的 int,或可由 int() 完整解析的 str;可选 minimum 下限。"""
@@ -256,6 +278,7 @@ class Unicom:
self.ttxc_newbie_list = []
self.ttxc_charge_level = {}
self.battle_page_referer = ""
self.uphone_cp_token = self.uphone_usr_token = self.uphone_device_id = ""
# 生成UUID用于签到
self.uuid = str(uuid.uuid4())
@@ -3738,6 +3761,387 @@ class Unicom:
await self.battle_draw_lottery()
# === 云手机积分 / 夏日刮一刮 ===
def uphone_biz_ok(self, data):
"""业务成功:data 必须为 dict 且 code 表示成功。"""
if not isinstance(data, dict):
return False
return data.get("code") in (200, "200", 0, "0")
def uphone_biz_data(self, r):
"""取出 req() 的 data 字段;非 dict 时返回 None。"""
if not isinstance(r, dict):
return None
data = r.get("data")
return data if isinstance(data, dict) else None
def uphone_app_headers(self, extra=None):
h = {
"User-Agent": UPHONE_APP_UA,
"Accept": "*/*",
"channel": UPHONE_CHANNEL,
"channelCode": UPHONE_CHANNEL,
"deviceId": self.uphone_device_id or "0",
}
if self.uphone_cp_token:
h["Authorization"] = self.uphone_cp_token
if extra:
h.update(extra)
return h
def uphone_h5_headers(self, extra=None):
h = {
"User-Agent": UPHONE_H5_UA,
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json; charset=UTF-8",
"Origin": "https://uphone.wostore.cn",
}
if self.uphone_usr_token:
h["X-USR-TOKEN"] = self.uphone_usr_token
if extra:
h.update(extra)
return h
async def uphone_sso_login(self):
"""ecs_token → ticket → cpToken"""
if not self.ecs_token:
self.tlog("无 ecs_token")
return False
self.uphone_device_id = hashlib.md5(
f"{self.mobile or self.uuid}".encode()
).hexdigest()
r = await self.req(
"GET",
"https://m.client.10010.com/edop_ng/getTicketByNative",
params={"token": self.ecs_token, "appId": UPHONE_EDOP_APP_ID},
headers={"User-Agent": UPHONE_APP_UA, "Accept": "*/*"},
)
body = self.uphone_biz_data(r)
ticket = body.get("ticket") if body else None
if not ticket:
self.tlog(f"换 ticket 失败: {api_response_err(r, 'getTicketByNative')}")
return False
r = await self.req(
"POST",
f"{UPHONE_H5API}/token-service/getTokenByTicket",
headers=self.uphone_app_headers({"Content-Type": "application/json"}),
json={"channel": UPHONE_CHANNEL, "ticket": ticket},
)
body = self.uphone_biz_data(r)
token = body.get("data") if body else None
if not (
body
and self.uphone_biz_ok(body)
and isinstance(token, str)
and token.startswith("eyJ")
):
self.tlog(f"换 cpToken 失败: {api_response_err(r, 'getTokenByTicket')}")
return False
self.uphone_cp_token = token
self.tlog("云手机 SSO 成功")
return True
async def uphone_activity_login(self, activity_id):
if not self.uphone_cp_token:
return False
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/user/login",
headers=self.uphone_h5_headers(),
json={
"identityType": "cloudPhoneLogin",
"code": self.uphone_cp_token,
"channelId": UPHONE_CHANNEL_H5,
"activityId": activity_id,
"device": "device",
},
)
body = self.uphone_biz_data(r)
nested = body.get("data") if body else None
tok = nested.get("user_token") if isinstance(nested, dict) else None
if not (body and self.uphone_biz_ok(body) and tok):
self.tlog(f"活动登录失败({activity_id}): {api_response_err(r, 'user/login')}")
return False
self.uphone_usr_token = str(tok)
return True
async def uphone_points_summary(self):
r = await self.req(
"GET",
f"{UPHONE_H5API}/activity-service/points/v1/summary",
headers=self.uphone_h5_headers(),
params={"activityCode": UPHONE_ACT_EXCHANGE},
)
body = self.uphone_biz_data(r)
if not body or not self.uphone_biz_ok(body):
return None
nested = body.get("data")
return nested if isinstance(nested, dict) else None
async def uphone_sign(self):
r = await self.req(
"GET",
f"{UPHONE_H5API}/activity-service/points/v1/sign/history",
headers=self.uphone_h5_headers(),
params={"activityCode": UPHONE_ACT_SIGN},
)
body = self.uphone_biz_data(r)
hist = body.get("data") if body and self.uphone_biz_ok(body) else None
records = hist.get("signRecords") if isinstance(hist, dict) else None
today = records[0] if isinstance(records, list) and records else {}
if not isinstance(today, dict):
today = {}
# signStatus=0 已签,1 未签
if today.get("signStatus") == 0:
self.tlog("今日已签到")
return True
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/points/v1/sign",
headers=self.uphone_h5_headers(),
json={"activityCode": UPHONE_ACT_SIGN},
)
body = self.uphone_biz_data(r)
if body and self.uphone_biz_ok(body):
self.tlog(f"签到成功: {body.get('msg') or 'ok'}")
return True
self.tlog(f"签到失败: {api_response_err(r, 'points/sign')}")
return False
async def uphone_task_list(self, activity_code):
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/user/task/list",
headers=self.uphone_h5_headers(),
json={"activityCode": activity_code},
)
body = self.uphone_biz_data(r)
if not body or not self.uphone_biz_ok(body):
return [], 0
task_list = body.get("taskList") or []
if not isinstance(task_list, list):
task_list = []
return task_list, safe_int(body.get("rafflesLeftCount"), 0)
async def uphone_task_logs(self, task_code, detail):
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/user/task/logs",
headers=self.uphone_h5_headers(),
json={
"logType": "01",
"logCode": task_code,
"logSource": "01",
"logDetail": detail,
},
)
body = self.uphone_biz_data(r)
return self.uphone_biz_ok(body), body
async def uphone_raffle_get(self, activity_code, task_code):
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/user/task/raffle/get",
headers=self.uphone_h5_headers(),
json={"activityCode": activity_code, "taskCode": task_code},
)
body = self.uphone_biz_data(r)
if isinstance(body, dict) and body.get("code") in (10301, "10301"):
self.tlog(f"领奖受限 {task_code}: {body.get('msg')}")
return False, body
return self.uphone_biz_ok(body), body
async def uphone_obtain_tasks(self):
"""积分任务:logs → 领奖(跳过需真实行为的任务)"""
if not await self.uphone_activity_login(UPHONE_ACT_OBTAIN):
return
tasks, _ = await self.uphone_task_list(UPHONE_ACT_OBTAIN)
claimed = logged = skipped = 0
for t in tasks:
if t.get("status") != "UNCLAIMED":
continue
code = str(t.get("taskCode") or "")
ok, _ = await self.uphone_raffle_get(UPHONE_ACT_OBTAIN, code)
if ok:
claimed += 1
self.tlog(f"领取积分任务 {code} {t.get('taskName')} +{t.get('pointsCount')}")
await asyncio.sleep(0.25)
tasks, _ = await self.uphone_task_list(UPHONE_ACT_OBTAIN)
for t in tasks:
code = str(t.get("taskCode") or "")
name = str(t.get("taskName") or code)
if t.get("status") != "INCOMPLETE":
continue
if code in UPHONE_OBTAIN_SKIP:
skipped += 1
continue
ok, body = await self.uphone_task_logs(code, name)
if not ok:
self.tlog(f"任务上报失败 {code}: {(body or {}).get('msg')}")
await asyncio.sleep(0.2)
continue
logged += 1
await asyncio.sleep(0.3)
tasks2, _ = await self.uphone_task_list(UPHONE_ACT_OBTAIN)
t2 = next((x for x in tasks2 if x.get("taskCode") == code), None)
if not t2 or t2.get("status") != "UNCLAIMED":
self.tlog(f"任务未达可领 {code} status={t2.get('status') if t2 else '?'}")
continue
ok, _ = await self.uphone_raffle_get(UPHONE_ACT_OBTAIN, code)
if ok:
claimed += 1
self.tlog(f"完成 {code} {name} +{t.get('pointsCount')}")
await asyncio.sleep(0.25)
summary = await self.uphone_points_summary()
bal = (summary or {}).get("balanceScoreNum")
self.tlog(
f"积分任务结束 claimed={claimed} logged={logged} skipped={skipped} 余额={bal}"
)
async def uphone_lottery10_remain(self):
r = await self.req(
"GET",
f"{UPHONE_H5API}/activity-service/points/v1/exchange/list",
headers=self.uphone_h5_headers(),
params={
"activityCode": UPHONE_ACT_EXCHANGE,
"token": self.uphone_cp_token,
},
)
body = self.uphone_biz_data(r)
if not body or not self.uphone_biz_ok(body):
return -1
goods = body.get("data") or []
if not isinstance(goods, list):
return -1
for g in goods:
if not isinstance(g, dict):
continue
if str(g.get("goodsId")) == UPHONE_GOODS_LOTTERY_10:
ru = g.get("remainUser")
return -1 if ru is None else safe_int(ru, 0)
return 0
async def uphone_lottery_10(self):
"""余额≥阈值且有剩余次数则十连抽"""
await self.uphone_activity_login(UPHONE_ACT_SIGN)
summary = await self.uphone_points_summary()
balance = safe_int((summary or {}).get("balanceScoreNum"), 0)
if balance < UPHONE_LOTTERY_COST:
self.tlog(f"积分不足十连 余额={balance} 需≥{UPHONE_LOTTERY_COST}")
return
remain = await self.uphone_lottery10_remain()
if remain == 0:
self.tlog("十连次数已用完 remainUser=0")
return
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/points/v1/lottery",
headers=self.uphone_h5_headers(),
json={
"activityCode": UPHONE_ACT_EXCHANGE,
"goodsId": UPHONE_GOODS_LOTTERY_10,
},
)
body = self.uphone_biz_data(r)
if not body or not self.uphone_biz_ok(body):
self.tlog(f"十连失败: {api_response_err(r, 'points/lottery')}")
return
raw_data = body.get("data")
data = raw_data if isinstance(raw_data, dict) else {}
raw_sm = data.get("summary")
sm = raw_sm if isinstance(raw_sm, dict) else {}
raw_results = data.get("results")
results = raw_results if isinstance(raw_results, list) else []
prizes = [
x.get("prizeName")
for x in results
if isinstance(x, dict) and x.get("isDrawn")
]
prize_text = ",".join(str(p) for p in prizes if p) if prizes else "无"
self.tlog(
f"十连完成 total={sm.get('total')} won={sm.get('won')} 奖品={prize_text}"
)
summary = await self.uphone_points_summary()
self.tlog(f"十连后余额={(summary or {}).get('balanceScoreNum')}")
async def uphone_summer_claim_one(self):
"""夏日刮一刮:仅领取 1 个可领任务的抽奖次数(优先 2508-01 去赚积分)"""
if not await self.uphone_activity_login(UPHONE_ACT_SUMMER):
return 0
tasks, left = await self.uphone_task_list(UPHONE_ACT_SUMMER)
unclaimed = [t for t in tasks if t.get("status") == "UNCLAIMED"]
if not unclaimed:
self.tlog(f"夏日无可领任务 rafflesLeft={left}")
return left
# 优先「去赚积分」,否则取列表第一个 UNCLAIMED
pick = next(
(t for t in unclaimed if str(t.get("taskCode")) == "2508-01"),
unclaimed[0],
)
code = str(pick.get("taskCode") or "")
name = pick.get("taskName") or code
ok, body = await self.uphone_raffle_get(UPHONE_ACT_SUMMER, code)
if ok:
self.tlog(f"夏日领次数成功 {code} {name}")
else:
self.tlog(
f"夏日领次数失败 {code}: {(body or {}).get('msg') or body}"
)
_, left2 = await self.uphone_task_list(UPHONE_ACT_SUMMER)
self.tlog(f"夏日抽奖次数 rafflesLeft={left}→{left2}")
return left2
async def uphone_summer_lottery(self, max_draws=20):
draws = 0
while draws < max_draws:
_, left = await self.uphone_task_list(UPHONE_ACT_SUMMER)
if left <= 0:
break
r = await self.req(
"POST",
f"{UPHONE_H5API}/activity-service/lottery",
headers=self.uphone_h5_headers(),
json={"activityCode": UPHONE_ACT_SUMMER},
)
body = self.uphone_biz_data(r)
if not body or not self.uphone_biz_ok(body):
self.tlog(f"夏日抽奖失败: {api_response_err(r, 'lottery')}")
break
draws += 1
self.tlog(
f"夏日抽奖#{draws} {body.get('prizeName') or body.get('msg')} "
f"type={body.get('prizeType')}"
)
await asyncio.sleep(0.4)
if draws == 0:
self.tlog("夏日无抽奖次数")
else:
self.tlog(f"夏日抽奖结束 draws={draws}")
return draws
async def uphone_task(self):
"""云手机:签到 → 积分任务 → 满额十连 → 夏日领1次次数并刮奖"""
self._task_tag = "云手机积分"
self.tlog("开始")
if not await self.uphone_sso_login():
return
if not await self.uphone_activity_login(UPHONE_ACT_SIGN):
return
await self.uphone_sign()
await self.uphone_obtain_tasks()
await self.uphone_lottery_10()
await self.uphone_summer_claim_one()
await self.uphone_summer_lottery()
self.tlog("结束")
# === 主任务 ===
async def run(self):
try:
@@ -3754,6 +4158,7 @@ class Unicom:
self.cloudpan_task,
self.cloud_battle_task,
self.shangdu_task,
self.uphone_task,
]:
try:
await task()