{"record":{"id":"5638ea0ce93d90af","repo":"datawhalechina/hello-agents","slug":"error-5638ea","errorCode":null,"errorMessage":"换一批失败","messagePattern":"换一批失败","errorType":"exception","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"Co-creation-projects/aatanxiao12-beep-YingQian/frontend/src/pages/ResultPage.tsx","lineNumber":75,"sourceCode":"    setStageIndex(1)\n    const timers: number[] = []\n    PROGRESS_STAGES.forEach((_, i) => {\n      if (i <= 1) return\n      timers.push(window.setTimeout(() => setStageIndex(i), 4_500 * (i - 1)))\n    })\n\n    const request = {\n      ...session.request,\n      exclude_ids: [\n        ...new Set([...session.request.exclude_ids, ...exclude_ids]),\n      ],\n      taste_profile,\n    }\n\n    try {\n      const res = await postRecommend(request)\n      if (!res.success || !res.data) {\n        throw new ApiError(res.message || '换一批失败')\n      }\n      const next: SessionPayload = {\n        request: {\n          ...request,\n          taste_profile: undefined,\n        },\n        result: {\n          ...res.data,\n          taste_profile: res.data.taste_profile ?? taste_profile,\n        },\n        message: res.message,\n      }\n      saveSession(next)\n      setSession(next)\n      setStageIndex(PROGRESS_STAGES.length - 1)\n    } catch (err) {\n      setError(err instanceof Error ? err.message : '换一批失败')\n    } finally {","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/aatanxiao12-beep-YingQian/frontend/src/pages/ResultPage.tsx#L57-L93","documentation":"'换一批失败' (change-batch failed) is thrown in beep-YingQian's ResultPage when postRecommend() resolves with success=false or missing data during a 'next batch' request. The ResultPage deliberately grows exclude_ids with everything already shown and passes the accumulated taste_profile, so each successive '换一批' shrinks the backend's candidate set until the recommender gives up.","triggerScenarios":"POST recommend with the merged request ({...session.request, exclude_ids: prior + shown, taste_profile}) returns 200 with success=false: candidate pool exhausted after N batch refreshes, taste_profile incompatible with remaining candidates, or the backend recommender errored. Fires with the generic text only when res.message is also empty.","commonSituations":"User refreshes the batch many times in one session (typical for narrow genres); long-persisted session with a huge exclude_ids list; backend message omitted on failure.","solutions":["Check the network response body — success:false with a message shows the real reason; this generic string appears only when message is empty.","If the pool is exhausted, cap exclude_ids (e.g. keep last K) or offer a 'start over' action that clears exclusions.","Verify taste_profile serialization (undefined vs missing key) matches what the backend schema accepts.","Have the backend return an explicit 'no more candidates' message and the UI render a friendly empty state instead of an error."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const MAX_EXCLUDE = 200;\nconst exclude_ids = [...new Set([...session.request.exclude_ids, ...exclude_idsShown])].slice(-MAX_EXCLUDE);\nif (exclude_ids.length >= MAX_EXCLUDE) {\n  offerFreshStart(); // pool likely drained\n}","typeGuard":"function isRecommendSuccess(res: unknown): res is { success: true; data: unknown; message?: string } {\n  return typeof res === 'object' && res !== null\n    && (res as { success?: unknown }).success === true\n    && 'data' in (res as object);\n}","tryCatchPattern":"try {\n  const res = await postRecommend(request);\n  if (!isRecommendSuccess(res)) {\n    if (/没有更多|no more|empty/i.test(res?.message || '')) {\n      renderNoMoreCandidates();\n    } else {\n      showError(res?.message || '换一批失败');\n    }\n    return;\n  }\n  // success path\n} catch (err) {\n  showError(err instanceof Error ? err.message : '换一批失败');\n}","preventionTips":["Bound the exclusion list growth on every batch refresh.","Send taste_profile as undefined (omit the key) rather than null if the backend schema rejects null.","Map backend 'no candidates' messages to a friendly empty state instead of an error alert."],"tags":["api-envelope","recommendation","pagination-drain","session-state"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}