{"record":{"id":"6ae5f781e929a5d0","repo":"jackwener/OpenCLI","slug":"raw-is-not-a-douyin-sec-uid","errorCode":null,"errorMessage":"${raw} is not a Douyin sec_uid","messagePattern":"(.+?) is not a Douyin sec_uid","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/douyin/user-videos.js","lineNumber":22,"sourceCode":"export const MAX_USER_VIDEOS_LIMIT = 20;\nexport const USER_VIDEO_COMMENT_CONCURRENCY = 4;\nexport const DEFAULT_COMMENT_LIMIT = 10;\n// sec_uid is base64url. Anything else (a nickname, a numeric UID, a search term)\n// still resolves to a Douyin page, so without this guard the scrape silently\n// returns some other account's videos.\n// Current Douyin sec_uid values use the stable `MS4wLjABAAAA` prefix followed\n// by a long base64url payload. Requiring the prefix prevents ordinary ASCII\n// nicknames and long numeric UIDs from passing a shape-only character check.\nconst SEC_UID_PATTERN = /^MS4wLjABAAAA[A-Za-z0-9_-]{8,}$/;\nconst SEC_UID_HINT = 'sec_uid looks like MS4wLjABAAAA… — it is the last path segment of https://www.douyin.com/user/<sec_uid>, not a nickname or a numeric UID.';\nexport function normalizeSecUid(input) {\n    const raw = String(input ?? '').trim();\n    if (!raw)\n        throw new ArgumentError('douyin user-videos requires a sec_uid', SEC_UID_HINT);\n    const fromUrl = raw.match(/douyin\\.com\\/user\\/([A-Za-z0-9_-]+)/);\n    const candidate = fromUrl ? fromUrl[1] : raw;\n    if (!SEC_UID_PATTERN.test(candidate))\n        throw new ArgumentError(`\"${raw}\" is not a Douyin sec_uid`, SEC_UID_HINT);\n    return candidate;\n}\nexport function normalizeUserVideosLimit(limit) {\n    const numeric = Number(limit);\n    if (!Number.isFinite(numeric))\n        return MAX_USER_VIDEOS_LIMIT;\n    return Math.min(MAX_USER_VIDEOS_LIMIT, Math.max(1, Math.round(numeric)));\n}\nexport function normalizeCommentLimit(limit) {\n    const numeric = Number(limit);\n    if (!Number.isFinite(numeric))\n        return DEFAULT_COMMENT_LIMIT;\n    return Math.min(DEFAULT_COMMENT_LIMIT, Math.max(1, Math.round(numeric)));\n}\nasync function mapInBatches(items, concurrency, mapper) {\n    const results = [];\n    for (let index = 0; index < items.length; index += concurrency) {\n        const chunk = items.slice(index, index + concurrency);","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/douyin/user-videos.js#L4-L40","documentation":"An ArgumentError thrown by normalizeSecUid when the provided value does not match SEC_UID_PATTERN (^MS4wLjABAAAA[A-Za-z0-9_-]{8,}$). Douyin sec_uids are long tokens beginning with 'MS4wLjABAAAA'; the pattern deliberately rejects nicknames and long numeric UIDs that would otherwise pass a shape-only character check. If the input is a profile URL, the last path segment is extracted first and the original raw string is reported in the message.","triggerScenarios":"Calling `douyin user-videos --sec-uid <value>` where value is a numeric UID, a nickname/handle, a truncated token, a full URL whose extracted segment still fails the pattern, or a sec_uid from another Douyin-like format.","commonSituations":"Passing the display name instead of the sec_uid; using the numeric 'user id' shown in some dashboards; copying an incomplete token (e.g. shell ate special chars); confusing Douyin sec_uid with TikTok's numeric IDs.","solutions":["Open https://www.douyin.com/user/<sec_uid> in a browser and copy the full token after /user/ (starts with MS4wLjABAAAA).","Verify the token is at least 'MS4wLjABAAAA' plus 8+ chars of [A-Za-z0-9_-] and contains no spaces or URL-encoding.","If you only have a nickname, resolve it to the profile in a browser first and take the sec_uid from the URL.","If passing a URL, ensure it is the canonical douyin.com/user/... form so the extractor picks the right segment."],"exampleFix":"// before\nawait run(['douyin', 'user-videos', '--sec-uid', '987654321']); // numeric uid\n// after\nawait run(['douyin', 'user-videos', '--sec-uid', 'MS4wLjABAAAAxxxxxxxxxxxxxxxx']);","handlingStrategy":"type-guard","validationCode":"const SEC_UID_RE = /^MS4wLjABAAAA[A-Za-z0-9_-]{8,}$/;\nconst raw = String(input ?? '').trim();\nconst candidate = raw.match(/douyin\\.com\\/user\\/([A-Za-z0-9_-]+)/)?.[1] ?? raw;\nif (!SEC_UID_RE.test(candidate)) throw new Error(`not a Douyin sec_uid: ${raw}`);","typeGuard":"function isDouyinSecUid(v) {\n  const s = typeof v === 'string' ? v.trim() : '';\n  const c = s.match(/douyin\\.com\\/user\\/([A-Za-z0-9_-]+)/)?.[1] ?? s;\n  return /^MS4wLjABAAAA[A-Za-z0-9_-]{8,}$/.test(c);\n}","tryCatchPattern":"try {\n  await run('douyin user-videos', { sec_uid });\n} catch (e) {\n  if (e.name === 'ArgumentError' && /not a Douyin sec_uid/.test(e.message)) {\n    console.error('sec_uid must start with MS4wLjABAAAA — copy the last path segment of the profile URL');\n    process.exitCode = 2;\n    return;\n  }\n  throw e;\n}","preventionTips":["Store sec_uids extracted from https://www.douyin.com/user/<sec_uid> URLs, never nicknames or numeric UIDs.","Pre-validate with the ^MS4wLjABAAAA… regex before invoking the command.","Watch for URL-encoding or truncation when copying tokens through shells."],"tags":["argument-error","douyin","format-validation","input-validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}