{"record":{"id":"1fddb64ba99ded4b","repo":"CherryHQ/cherry-studio","slug":"qr-login-confirmed-but-the-api-did-not-return-bot","errorCode":null,"errorMessage":"QR login confirmed, but the API did not return bot credentials","messagePattern":"QR login confirmed, but the API did not return bot credentials","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/wechat/WeChatProtocol.ts","lineNumber":632,"sourceCode":"        throw new Error('Login cancelled')\n      }\n\n      const status = await pollQrStatus(options.baseUrl, qr.qrcode)\n\n      if (status.status !== lastStatus) {\n        if (status.status === 'scaned') {\n          logger.info('QR code scanned, waiting for confirmation')\n        } else if (status.status === 'confirmed') {\n          logger.info('Login confirmed')\n        } else if (status.status === 'expired') {\n          logger.info('QR code expired', { attempt: qrRetries + 1, maxAttempts: MAX_QR_RETRIES })\n        }\n        lastStatus = status.status\n      }\n\n      if (status.status === 'confirmed') {\n        if (!status.bot_token || !status.ilink_bot_id || !status.ilink_user_id) {\n          throw new Error('QR login confirmed, but the API did not return bot credentials')\n        }\n\n        const credentials: Credentials = {\n          token: status.bot_token,\n          baseUrl: status.baseurl ?? options.baseUrl,\n          accountId: status.ilink_bot_id,\n          userId: status.ilink_user_id\n        }\n        await saveCredentials(credentials, options.tokenPath)\n        return credentials\n      }\n\n      if (status.status === 'expired') break\n\n      await delay(QR_POLL_INTERVAL_MS)\n    }\n\n    qrRetries++","sourceCodeStart":614,"sourceCodeEnd":650,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/wechat/WeChatProtocol.ts#L614-L650","documentation":"Thrown by loginFlow() when pollQrStatus returns status:'confirmed' but the response is missing bot_token, ilink_bot_id, or ilink_user_id. The user successfully scanned and confirmed the QR, but the WeChat iLink backend did not return the credentials needed to construct a Credentials object. This is a protocol/server defect, not a user action — the login reached the final step but the payload was incomplete.","triggerScenarios":"pollQrStatus returns {status:'confirmed'} and one or more of bot_token, ilink_bot_id, ilink_user_id is undefined/empty. The QrStatusResponseSchema (WeChatProtocol.ts:127) marks these optional precisely because the server may omit them. This would indicate a server-side issue, a protocol version mismatch, or an edge case where confirmation arrived but credential issuance had not completed server-side.","commonSituations":"Rare. Could occur if the WeChat iLink backend had a partial failure during credential issuance; if the reverse-engineered protocol expects a follow-up call to fetch credentials that is not implemented; if a protocol change moved the credentials to a different response field; race condition where the status was polled in the narrow window between user confirmation and server-side credential provisioning.","solutions":["Retry the login after clearing credentials — if it was a transient server issue, a fresh QR flow may return complete credentials.","Poll the status once more after a short delay before giving up — the server may provision credentials a beat after the 'confirmed' status.","Delete the token file and run WeixinBot.login({force:true}) to start completely fresh.","If persistent, the protocol may have changed — verify the QrStatusResponseSchema fields against current iLink responses."],"exampleFix":"// before — single poll, throws on first confirmed-without-credentials\nif (status.status === 'confirmed') {\n  if (!status.bot_token || !status.ilink_bot_id || !status.ilink_user_id) {\n    throw new Error('QR login confirmed, but the API did not return bot credentials')\n  }\n  // ...\n}\n\n// after — one bounded re-poll to absorb a server-side provisioning race\nif (status.status === 'confirmed') {\n  let s = status\n  for (let i = 0; i < 3 && (!s.bot_token || !s.ilink_bot_id || !s.ilink_user_id); i++) {\n    await delay(1_000)\n    s = await pollQrStatus(options.baseUrl, qr.qrcode)\n  }\n  if (!s.bot_token || !s.ilink_bot_id || !s.ilink_user_id) {\n    throw new Error('QR login confirmed, but the API did not return bot credentials')\n  }\n  // proceed with s\n}","handlingStrategy":"retry","validationCode":"// You cannot prevent a server-side omission, but you can poll once more before failing.\n// Wrap the confirmed-status check in a bounded re-poll:\nasync function awaitConfirmedCredentials(\n  baseUrl: string,\n  qrCode: string,\n  maxRepolls = 3\n): Promise<{ bot_token: string; ilink_bot_id: string; ilink_user_id: string }> {\n  for (let i = 0; i < maxRepolls; i++) {\n    const s = await pollQrStatus(baseUrl, qrCode)\n    if (s.status === 'confirmed' && s.bot_token && s.ilink_bot_id && s.ilink_user_id) {\n      return { bot_token: s.bot_token, ilink_bot_id: s.ilink_bot_id, ilink_user_id: s.ilink_user_id }\n    }\n    if (s.status !== 'confirmed') throw new Error(`QR status regressed to ${s.status}`)\n    await delay(1_000)\n  }\n  throw new Error('QR login confirmed, but the API did not return bot credentials')\n}","typeGuard":"function hasFullCredentials(s: QrStatusResponse): boolean {\n  return Boolean(s.bot_token && s.ilink_bot_id && s.ilink_user_id)\n}","tryCatchPattern":"try {\n  const credentials = await bot.login({ signal })\n} catch (e) {\n  if (e instanceof Error && /did not return bot credentials/.test(e.message)) {\n    // Retry the whole login once — server-side provisioning race\n    logger.warn('WeChat confirmed without credentials, retrying login')\n    await rm(tokenPath, { force: true })\n    return await bot.login({ force: true, signal })\n  }\n  throw e\n}","preventionTips":["Poll once or twice more after a 'confirmed' status before failing — the server may provision credentials a beat later.","If the issue persists, the reverse-engineered QrStatusResponseSchema fields may have moved — verify against current iLink responses.","Clear the credentials file before retrying a fresh QR flow to avoid loadCredentials short-circuiting.","Log the raw confirmed-status payload when this throws to capture which field was missing."],"tags":["wechat","login","qr","protocol","server-error"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}