@@ -151,6 +151,16 @@ async def _depart(self, credential: CodeBuddyCredential, result: GrowthResult) -
151151 location = locations [0 ]
152152 try :
153153 data = await self ._client .depart (credential , location .id )
154+ except GrowthRejected as error :
155+ if _is_no_buddy (error ):
156+ # 还没有 Buddy 时派出必然失败——这是账号状态而非故障,而且与其他
157+ # 17 个任务的 first_buddy 是同一个根因,说一遍就够(不刷屏、不算失败)
158+ result .steps .append (GrowthStep (
159+ "派 Buddy" , StepStatus .IDLE , "尚未领取 Buddy(在客户端新建任务并发起对话)" ,
160+ reportable = True ))
161+ return
162+ self ._note_error (result , "派 Buddy" , error )
163+ return
154164 except Exception as error : # noqa: BLE001
155165 self ._note_error (result , "派 Buddy" , error )
156166 return
@@ -183,9 +193,21 @@ async def _tasks(self, credential: CodeBuddyCredential,
183193
184194 async def _accept_pending (self , credential : CodeBuddyCredential , result : GrowthResult ,
185195 tasks : list [GrowthTaskItem ]) -> bool :
186- """接单:一批提交多个 task_code;逐条读 results,失败必须报出来。"""
196+ """接单:一批提交多个 task_code,逐条读 results。
197+
198+ 失败要分三类对待,**不是**一律记 FAILED:
199+ - `prerequisite not met: <code>`:前置任务未完成。这是常态(新账号全部任务都
200+ 被 first_buddy 门住),而且**众多任务共享同一个前置条件**——逐条报会让报告
201+ 变成十几行同样的噪音,掩盖「其实只需做一件事」。按原因归并成一条汇总。
202+ - `task does not require acceptance`:上游说这个任务不需要接单,是**正常应答**
203+ 而非失败(此前当失败报,误导用户以为出了问题)。
204+ - 其余:真正需要人看的失败,逐条报出。
205+ """
187206 pending = [task .task_code for task in tasks if task .needs_accept and task .task_code ]
188207 titles = {task .task_code : task .title for task in tasks }
208+ accepted = 0
209+ blocked : dict [str , list [str ]] = {} # 前置条件 → 受影响的标题
210+ others : list [tuple [str , str ]] = [] # (标题, 上游原文)
189211 for start in range (0 , len (pending ), ACCEPT_BATCH_SIZE ):
190212 batch = pending [start :start + ACCEPT_BATCH_SIZE ]
191213 try :
@@ -195,13 +217,31 @@ async def _accept_pending(self, credential: CodeBuddyCredential, result: GrowthR
195217 for item in results :
196218 code = item .get ("task_code" )
197219 title = titles .get (code , code )
198- if item .get ("status" ) == "error" :
199- message = str (item .get ("message" ) or "未说明原因" )
200- result .steps .append (GrowthStep (
201- "领取任务" , StepStatus .FAILED , f"「{ title } 」失败:{ message } " ))
202- else :
220+ if item .get ("status" ) != "error" :
221+ accepted += 1
203222 result .steps .append (GrowthStep (
204223 "领取任务" , StepStatus .DONE , f"「{ title } 」(进度开始计)" ))
224+ continue
225+ message = str (item .get ("message" ) or "未说明原因" )
226+ if _prerequisite_of (message ):
227+ blocked .setdefault (_prerequisite_of (message ) or "" , []).append (title )
228+ elif "does not require acceptance" in message :
229+ # 正常应答:该任务不需要接单,不用管它(下一步会照常尝试领奖)
230+ continue
231+ else :
232+ others .append ((title , message ))
233+ if accepted :
234+ result .steps .append (GrowthStep (
235+ "接单完成" , StepStatus .DONE , f"共 { accepted } 个任务开始计进度" ))
236+ for reason , titles_blocked in blocked .items ():
237+ # reportable=True:这是用户需要知道并去处理的事,不能被摘要过滤掉
238+ result .steps .append (GrowthStep (
239+ "接单受阻" , StepStatus .IDLE ,
240+ f"{ len (titles_blocked )} 个任务需先完成「{ _prerequisite_label (reason )} 」"
241+ f"(在官方客户端操作后自动解除)" , reportable = True ))
242+ for title , message in others :
243+ result .steps .append (GrowthStep (
244+ "领取任务" , StepStatus .FAILED , f"「{ title } 」失败:{ message } " ))
205245 return True
206246
207247 async def _claim_completed (self , credential : CodeBuddyCredential , result : GrowthResult ,
@@ -422,10 +462,17 @@ def _fmt(value: Any) -> str:
422462 return str (int (number )) if number .is_integer () else f"{ number :g} "
423463
424464
465+ def _in_report (step ) -> bool :
466+ """该步是否进一行汇报:显式声明优先,否则 DONE/FAILED 默认进。"""
467+ if step .reportable is not None :
468+ return step .reportable
469+ return step .status in (StepStatus .DONE , StepStatus .FAILED )
470+
471+
425472def _report (result : GrowthResult ) -> str :
426473 """一行中文汇报(存 events 表 / 直接展示给用户)。"""
427474 parts = [f"{ step .name } :{ step .detail } " if step .detail else step .name
428- for step in result .steps if step . status in ( StepStatus . DONE , StepStatus . FAILED )]
475+ for step in result .steps if _in_report ( step )]
429476 if not parts :
430477 parts = ["成长中心无可领取项" ]
431478 tail = []
@@ -439,3 +486,32 @@ def _report(result: GrowthResult) -> str:
439486 if result .credit :
440487 tail .append (f"本次 +共 { _fmt (result .credit )} 积分" )
441488 return ";" .join (parts ) + (f"({ ',' .join (tail )} )" if tail else "" )
489+
490+ # 前置条件在 tasks/accept 的 results 里以 "prerequisite not met: <task_code>" 出现。
491+ # 已知的前置任务展示名(接口不给标题时回落成 task_code 本身)。
492+ _PREREQUISITE_LABELS = {"first_buddy" : "领取一只 Buddy(在客户端新建任务并发起对话)" }
493+
494+
495+ def _is_no_buddy (error : GrowthRejected ) -> bool :
496+ """派出 Buddy 是否因为「还没有 Buddy」被拒——账号状态,不是故障。
497+
498+ 上游对这种前置缺失回 400 + `no active buddy`。当故障记会让新账号的报告里
499+ 混进一条并不需要处理的"失败",而它与 17 个任务的 first_buddy 是同一根因。
500+ """
501+ return error .status == 400 and "buddy" in error .message .lower ()
502+
503+
504+ def _prerequisite_of (message : str ) -> str | None :
505+ """从失败消息里取出前置任务 code;不是前置条件问题则返回 None。"""
506+ marker = "prerequisite not met:"
507+ lowered = message .lower ()
508+ if marker not in lowered :
509+ return None
510+ return message [lowered .index (marker ) + len (marker ):].strip ().split ()[0 ].strip (".,;" ) \
511+ if message [lowered .index (marker ) + len (marker ):].strip () else ""
512+
513+
514+ def _prerequisite_label (reason : str ) -> str :
515+ """前置条件的可读标签:接口只给 task_code,这里补一句用户该做什么。"""
516+ return _PREREQUISITE_LABELS .get (reason , reason )
517+
0 commit comments