{"record":{"id":"e3d2afadd327097d","repo":"CherryHQ/cherry-studio","slug":"invalid-credentials-format-in-tokenpath","errorCode":null,"errorMessage":"Invalid credentials format in ${tokenPath}","messagePattern":"Invalid credentials format in (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/wechat/WeChatProtocol.ts","lineNumber":561,"sourceCode":"  return {\n    from_user_id: '',\n    to_user_id: userId,\n    client_id: randomUUID(),\n    message_type: MessageType.BOT,\n    message_state: MessageState.FINISH,\n    context_token: contextToken,\n    item_list: [{ type: MessageItemType.TEXT, text_item: { text } }]\n  }\n}\n\n// --------------- Auth ---------------\n\nasync function loadCredentials(tokenPath: string): Promise<Credentials | undefined> {\n  try {\n    const raw = await readFile(tokenPath, 'utf8')\n    const result = CredentialsSchema.safeParse(JSON.parse(raw))\n    if (!result.success) {\n      throw new Error(`Invalid credentials format in ${tokenPath}`)\n    }\n    return result.data\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n      return undefined\n    }\n    throw error\n  }\n}\n\nasync function saveCredentials(credentials: Credentials, tokenPath: string): Promise<void> {\n  await mkdir(path.dirname(tokenPath), { recursive: true, mode: 0o700 })\n  await writeFile(tokenPath, `${JSON.stringify(credentials, null, 2)}\\n`, { mode: 0o600 })\n  await chmod(tokenPath, 0o600)\n}\n\nasync function clearCredentials(tokenPath: string): Promise<void> {\n  await rm(tokenPath, { force: true })","sourceCodeStart":543,"sourceCodeEnd":579,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/wechat/WeChatProtocol.ts#L543-L579","documentation":"Thrown by loadCredentials() in WeChatProtocol when the credentials file at tokenPath exists and parses as JSON but fails Zod validation against CredentialsSchema ({token:string, baseUrl:string, accountId:string, userId:string}). This is a persisted-state corruption: the file is present but its shape does not match what the bot expects. ENOENT (file missing) is handled separately and returns undefined — only a present-but-malformed file throws.","triggerScenarios":"readFile succeeds, JSON.parse succeeds (otherwise it would throw SyntaxError, not this message — but note the catch only re-throws non-ENOENT errors, so a SyntaxError from JSON.parse would propagate, not this message), then CredentialsSchema.safeParse fails. This means the JSON is valid but missing required fields or has wrong types: e.g. {token:'x'} without baseUrl/accountId/userId, or fields with null where strings are required.","commonSituations":"An older app version wrote a credentials file with a different schema (e.g. missing accountId added in a later migration); manual editing of the token file corrupted it; a partial write left a truncated JSON object (though that usually fails JSON.parse first); the file was overwritten by another process writing a different JSON structure.","solutions":["Delete the credentials file at tokenPath (it is at feature.agents.channels/weixin_bot_<channelId>.json per WeChatAdapter.ts:22) to force a fresh QR login.","Validate the file contents manually — it must have all four string fields: token, baseUrl, accountId, userId.","If this is a migration issue, write a one-time migration that backfills missing fields or removes the old file.","Run WeixinBot.login({force:true}) to regenerate the credentials file from a new QR scan."],"exampleFix":"// before — corrupt file blocks startup; user must find and delete it manually\nconst existing = await loadCredentials(options.tokenPath) // throws\n\n// after — caller treats invalid credentials as 'no credentials' and re-logins\nlet existing: Credentials | undefined\ntry {\n  existing = await loadCredentials(options.tokenPath)\n} catch (e) {\n  logger.warn('Credentials file corrupt, forcing re-login', { error: (e as Error).message })\n  await rm(options.tokenPath, { force: true })\n}\nif (existing) return existing\n// proceed to QR login","handlingStrategy":"validation","validationCode":"// Validate the credentials file shape BEFORE handing it to loadCredentials,\n// or treat loadCredentials failure as 'no credentials' and force re-login.\nconst CREDENTIALS_SHAPE = {\n  token: 'string',\n  baseUrl: 'string',\n  accountId: 'string',\n  userId: 'string'\n} as const\n\nfunction looksLikeCredentials(raw: unknown): boolean {\n  if (typeof raw !== 'object' || raw === null) return false\n  return Object.entries(CREDENTIALS_SHAPE).every(\n    ([k, t]) => typeof (raw as Record<string, unknown>)[k] === t\n  )\n}\n\n// Safer wrapper: corrupt file → force re-login instead of propagating the throw\nasync function loadCredentialsOrForceRelogin(tokenPath: string): Promise<Credentials | undefined> {\n  try {\n    return await loadCredentials(tokenPath)\n  } catch (e) {\n    logger.warn('Credentials file invalid, removing to force re-login', { error: (e as Error).message })\n    await rm(tokenPath, { force: true })\n    return undefined\n  }\n}","typeGuard":"// Reuse the Zod schema already defined in WeChatProtocol.ts:139\n// CredentialsSchema = z.object({ token: z.string(), baseUrl: z.string(), accountId: z.string(), userId: z.string() })\nfunction isValidCredentials(raw: unknown): raw is Credentials {\n  return CredentialsSchema.safeParse(raw).success\n}","tryCatchPattern":"// In the login path, treat an invalid credentials file as 'no credentials'\n// rather than letting the throw propagate to the user as a hard error.\nlet existing: Credentials | undefined\ntry {\n  existing = await loadCredentials(options.tokenPath)\n} catch {\n  existing = undefined // force QR login\n}\nif (existing) return existing","preventionTips":["Never hand-edit the weixin_bot_<channelId>.json file — it is written by saveCredentials with a fixed schema.","If migrating schema, add a migration step that backfills new fields or removes the old file before the new code reads it.","Run WeixinBot.login({force:true}) to regenerate a valid credentials file after schema changes.","Write a backup of credentials before schema-migrating, and validate with CredentialsSchema.safeParse before trusting."],"tags":["wechat","credentials","filesystem","validation","corruption"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}