{"record":{"id":"310e26a37a8e522f","repo":"jackwener/OpenCLI","slug":"invalid-douban-subject-id-subjectid","errorCode":null,"errorMessage":"Invalid Douban subject ID: ${subjectId}","messagePattern":"Invalid Douban subject ID: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/douban/utils.js","lineNumber":128,"sourceCode":"            if (attempt >= attempts - 1 || !isDetachedPageError(error)) {\n                throw error;\n            }\n        }\n    }\n    throw lastError;\n}\nfunction buildDoubanSearchUrl(type, keyword) {\n    const url = new URL(`https://search.douban.com/${encodeURIComponent(type)}/subject_search`);\n    url.searchParams.set('search_text', String(keyword || ''));\n    if (String(type || '').trim() === 'book') {\n        url.searchParams.set('cat', '1001');\n    }\n    return url.toString();\n}\nexport function normalizeDoubanSubjectId(subjectId) {\n    const normalized = String(subjectId || '').trim();\n    if (!/^\\d+$/.test(normalized)) {\n        throw new ArgumentError(`Invalid Douban subject ID: ${subjectId}`);\n    }\n    return normalized;\n}\nexport function promoteDoubanPhotoUrl(url, size = 'l') {\n    const normalized = String(url || '').trim();\n    if (!normalized)\n        return '';\n    if (/^[a-z]+:/i.test(normalized) && !/^https?:/i.test(normalized))\n        return '';\n    return normalized.replace(/\\/view\\/photo\\/[^/]+\\/public\\//, `/view/photo/${size}/public/`);\n}\nexport function resolveDoubanPhotoAssetUrl(candidates, baseUrl = '') {\n    for (const candidate of candidates) {\n        const normalized = String(candidate || '').trim();\n        if (!normalized)\n            continue;\n        let resolved = normalized;\n        try {","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/douban/utils.js#L110-L146","documentation":"normalizeDoubanSubjectId requires the subject id to be a pure digit string. It throws ArgumentError for anything else, since douban subject ids are numeric and downstream URLs are built from the normalized value.","triggerScenarios":"Passing a full douban URL (e.g. https://movie.douban.com/subject/1292052/) instead of the bare id; an id with whitespace, letters, or a trailing slash; null/undefined/empty input.","commonSituations":"Copying the whole subject URL from the browser; mixing up douban.com/book/subject/ URL forms; a spreadsheet column containing URLs rather than ids.","solutions":["Pass only the numeric id, e.g. normalizeDoubanSubjectId('1292052')","If you have a URL, extract the digits first: url.match(/subject\\/(\\d+)/)[1]","Trim surrounding whitespace and confirm the value is not empty"],"exampleFix":"// before\nnormalizeDoubanSubjectId('https://movie.douban.com/subject/1292052/');\n// after\nconst m = url.match(/subject\\/(\\d+)/);\nnormalizeDoubanSubjectId(m[1]);","handlingStrategy":"validation","validationCode":"const s = String(subjectId ?? '').trim();\nif (!/^\\d+$/.test(s)) throw new Error(`Subject id must be digits; got ${subjectId}`);","typeGuard":"function isDoubanSubjectId(v) { return /^\\d+$/.test(String(v ?? '').trim()); }","tryCatchPattern":"try { await doubanSubject(subjectId); } catch (e) { if (e.name === 'ArgumentError') { const m = String(subjectId).match(/subject\\/(\\d+)/); if (m) return doubanSubject(m[1]); } throw e; }","preventionTips":["Extract the numeric id from URLs with /subject\\/(\\d+)/ before calling","Trim inputs and reject empty values at the CLI boundary","Keep subject ids as plain digit strings in stored config"],"tags":["argument-error","input-validation","douban","regex-match"],"backgroundTag":"invalid-argument-format","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}