3131 new_machine_identity ,
3232 parse_callback_url ,
3333)
34- from .credential import TraeCredential , parse_credential
34+ from .credential import TraeCredential , checkin_device_id , parse_credential
3535from .events import (
3636 AGENT_HOST ,
3737 APP_ID ,
5050 UpstreamProtocolViolation ,
5151)
5252
53+ # 9074 时的设备号轮换次数(同账号换不同的派生设备号,上游按 uid 记账不影响发放)
54+ CHECKIN_DEVICE_GENERATIONS = 3
5355STREAM_TIMEOUT = httpx .Timeout (connect = 10.0 , read = None , write = 10.0 , pool = 10.0 )
5456SHORT_TIMEOUT = httpx .Timeout (30.0 )
5557
@@ -230,12 +232,17 @@ def solo_headers(credential: TraeCredential, *, stream: bool = True) -> dict[str
230232 return headers
231233
232234
233- def ug_headers (credential : TraeCredential ) -> dict [str , str ]:
234- """签到/积分端点头(api.trae.cn)。对照原实现 UgHeaders。
235+ def ug_headers (credential : TraeCredential , * , device_id : str = "" ) -> dict [str , str ]:
236+ """签到/积分端点头(api.trae.cn)。
235237
236- 论坛实测(topic/180147):设备头是签到 API 的隐藏必填项。
237- 缺 X-Machine-Id / X-Device-Id 时 status 直接返回 9004(参数错误);
238- 补齐后请求格式正确,只剩 9074(服务器繁忙/限流)。uid 对应 auth.userId。
238+ 设备头是签到 API 的隐藏必填项(论坛实测 topic/180147):缺 X-Device-Id 时
239+ status 直接返回 9004(参数错误)。
240+
241+ **X-Device-Id 必须是 16 位纯数字**。用凭证自带的 deviceId(登录 URL 用的
242+ hex32)调 claim 会稳定得到 9074「当前参与用户太多」——这个码看起来像限流,
243+ 实际是设备标识格式不符(实测:hex32 → 9074;16 位数字 / 随机 hex → code:0;
244+ 空串 → 9004)。默认取 `checkin_device_id(uid)` 的派生值,调用方可在 9074 时
245+ 传入轮换后的代次值。
239246 """
240247 headers = {
241248 "Content-Type" : "application/json" ,
@@ -244,10 +251,12 @@ def ug_headers(credential: TraeCredential) -> dict[str, str]:
244251 "Authorization" : f"Cloud-IDE-JWT { credential .access_token } " ,
245252 "X-User-Region" : "CN" ,
246253 }
254+ # machine_id 保持登录时那一对(它不是签到的校验项,换掉反而可能与登录态不匹配)
247255 if credential .machine_id :
248256 headers ["X-Machine-Id" ] = credential .machine_id
249- if credential .device_id :
250- headers ["X-Device-Id" ] = credential .device_id
257+ effective = device_id or checkin_device_id (credential .uid ) or credential .device_id
258+ if effective :
259+ headers ["X-Device-Id" ] = effective
251260 if credential .uid :
252261 headers ["X-Uid" ] = credential .uid
253262 return headers
@@ -388,20 +397,24 @@ async def fetch_quota(self, credential: TraeCredential) -> Quota:
388397 used += float (pack_used ) if isinstance (pack_used , (int , float )) else 0.0
389398 return Quota (remaining = max (0.0 , limit - used ), total = limit , probed_at = int (time .time ()))
390399
391- async def fetch_checkin_status (self , credential : TraeCredential ) -> dict [str , Any ]:
400+ async def fetch_checkin_status (self , credential : TraeCredential ,
401+ * , generation : int = 0 ) -> dict [str , Any ]:
392402 """checkin_credits/status:checked_in / credits / enable。"""
393403 data = await self ._post_json (
394- f"{ self .ug_host } { trae_events .EP_CHECKIN_STATUS } " , {}, ug_headers (credential ))
404+ f"{ self .ug_host } { trae_events .EP_CHECKIN_STATUS } " , {},
405+ ug_headers (credential , device_id = checkin_device_id (credential .uid , generation )))
395406 return {
396407 "checked_in" : bool (data .get ("checked_in" )),
397408 "credits" : data .get ("credits" ),
398409 "enable" : bool (data .get ("enable" )),
399410 }
400411
401- async def claim_checkin (self , credential : TraeCredential ) -> dict [str , Any ] | None :
412+ async def claim_checkin (self , credential : TraeCredential ,
413+ * , generation : int = 0 ) -> dict [str , Any ] | None :
402414 """checkin_credits/claim:领取当日积分。"""
403415 return await self ._post_json (
404- f"{ self .ug_host } { trae_events .EP_CHECKIN_CLAIM } " , {}, ug_headers (credential ))
416+ f"{ self .ug_host } { trae_events .EP_CHECKIN_CLAIM } " , {},
417+ ug_headers (credential , device_id = checkin_device_id (credential .uid , generation )))
405418
406419 async def refresh_token (self , credential : TraeCredential ) -> TraeCredential :
407420 """ExchangeToken;失败不改写原凭证字段。"""
@@ -603,25 +616,36 @@ async def checkin(self, credential_data: dict) -> CheckinResult:
603616 """TRAE 签到:先查状态,未签且可签才领取。
604617
605618 上游没有独立的「已签到」错误码,status.checked_in 就是已签语义。
606- claim 可能返回 HTTP 200 + 业务码非 0 的软失败(如实测 9074
607- 「当前参与用户太多」),必须解析 code,否则会误报成功。
619+ claim 可能返回 HTTP 200 + 业务码非 0 的软失败,必须解析 code,否则会误报成功。
620+
621+ 9074「当前参与用户太多」**不是限流,而是设备标识格式不符**(实测:hex32
622+ → 9074,16 位数字 → success)。默认已经用派生的 16 位数字设备号,所以
623+ 正常不该再遇到;这里保留一次轮换代次重试,应对「派生值恰好在服务端侧不可用」
624+ 这种未知情况(同账号换设备号不影响领取结果,上游按 uid 记账)。
608625 """
609626 credential = TraeCredential .from_dict (credential_data )
610- status = await self .client .fetch_checkin_status (credential )
611- if status ["checked_in" ]:
612- return CheckinResult (ok = True , credit = None , message = "今天已签到" ,
613- already_checked_in = True )
614- if not status ["enable" ]:
615- return CheckinResult (ok = False , message = "当前账号不可签到" )
616- claim = await self .client .claim_checkin (credential ) # _post_json 保证 dict
617- code = claim .get ("code" )
618- if code not in (0 , None ):
627+ for generation in range (CHECKIN_DEVICE_GENERATIONS ):
628+ status = await self .client .fetch_checkin_status (
629+ credential , generation = generation )
630+ if status ["checked_in" ]:
631+ return CheckinResult (ok = True , credit = None , message = "今天已签到" ,
632+ already_checked_in = True )
633+ if not status ["enable" ]:
634+ return CheckinResult (ok = False , message = "当前账号不可签到" )
635+ claim = await self .client .claim_checkin (credential , generation = generation )
636+ code = claim .get ("code" )
637+ if code in (0 , None ):
638+ # 领取成功 → 回查 status 带回当前积分总额
639+ latest = await self .client .fetch_checkin_status (
640+ credential , generation = generation )
641+ return CheckinResult (ok = True , credit = latest .get ("credits" ), code = 0 )
642+ if code == 9074 and generation + 1 < CHECKIN_DEVICE_GENERATIONS :
643+ continue # 换一个派生设备号再试(同账号其它代次)
619644 # 软失败:不抛异常(后台任务按失败计数,当日不封账,下轮重试)
620645 return CheckinResult (ok = False , code = int (code ),
621646 message = str (claim .get ("message" ) or "claim 失败" ))
622- # 领取成功 → 回查 status 带回当前积分总额
623- latest = await self .client .fetch_checkin_status (credential )
624- return CheckinResult (ok = True , credit = latest .get ("credits" ), code = 0 )
647+ # 循环必然在每轮内返回;保留兜底让静态检查满意
648+ return CheckinResult (ok = False , message = "签到未完成" ) # pragma: no cover
625649
626650 # ------------------------------------------------- callback 轨道(Q17=C)
627651
0 commit comments