{"record":{"id":"5b420fbaa78958c6","repo":"jackwener/OpenCLI","slug":"malformed-toutiao-recommend-row-missing-group-id","errorCode":null,"errorMessage":"Malformed toutiao recommend row: missing group_id and article source_url.","messagePattern":"Malformed toutiao recommend row: missing group_id and article source_url\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/toutiao/utils.js","lineNumber":228,"sourceCode":"        }\n        return url;\n    } catch (error) {\n        if (error instanceof CommandExecutionError) throw error;\n        throw new CommandExecutionError(`Malformed toutiao recommend row: invalid source_url ${JSON.stringify(raw)}.`);\n    }\n}\n\nfunction articleIdFromToutiaoPath(pathname) {\n    return String(pathname || '').match(/^\\/(?:group|article)\\/([A-Za-z0-9_-]+)\\/?$/)?.[1] || '';\n}\n\nfunction buildGroupIdentity(sourceUrl, rawGroupId) {\n    const source = firstPartyToutiaoUrl(sourceUrl);\n    const sourceId = source ? articleIdFromToutiaoPath(source.pathname) : '';\n    const explicitId = trimOrNull(rawGroupId);\n    const groupId = explicitId || trimOrNull(sourceId);\n    if (!groupId) {\n        throw new CommandExecutionError('Malformed toutiao recommend row: missing group_id and article source_url.');\n    }\n    if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {\n        throw new CommandExecutionError(`Malformed toutiao recommend row: invalid group_id ${JSON.stringify(rawGroupId)}.`);\n    }\n    if (sourceId && explicitId && explicitId !== sourceId) {\n        throw new CommandExecutionError(`Malformed toutiao recommend row: group_id/source_url mismatch for ${JSON.stringify(rawGroupId)}.`);\n    }\n    return {\n        groupId,\n        url: `https://www.toutiao.com/group/${groupId}/`,\n    };\n}\n\nfunction formatBehotTime(value) {\n    const seconds = Number(value);\n    if (!Number.isFinite(seconds) || seconds <= 0) return null;\n    return `${new Date(seconds * 1000).toISOString().slice(0, 19)}Z`;\n}","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/toutiao/utils.js#L210-L246","documentation":"buildGroupIdentity derives a group id for a recommend row from either an explicit group_id or the article id embedded in the source_url path (/group/<id>/ or /article/<id>/). When both are absent (or the URL itself was unusable), no identity can be built, so it throws this CommandExecutionError — the row cannot be deduplicated or linked.","triggerScenarios":"A recommend row with no group_id field AND a source_url that is null, empty, off-domain, or a toutiao.com URL whose pathname is not /group/<id>/ or /article/<id>/ (e.g. a homepage or topic URL).","commonSituations":"Feed rows for non-article content (videos, user pages, special topics) lacking ids; scraping a page where group_id was renamed or moved; fixtures missing the id fields entirely.","solutions":["Ensure each row carries either a group_id or a toutiao.com /group/<id>/ or /article/<id>/ source_url.","Skip rows lacking both identifiers and log them instead of failing the whole run.","If the URL is a non-article toutiao page, extract the id with your own path rule before calling.","Check for upstream API changes — a renamed field (e.g. group_id -> item_id) needs a mapping update."],"exampleFix":"// before\nconst identity = buildGroupIdentity(row.source_url, row.group_id);\n// after\nif (!row.group_id && !/\\/group|article\\/\\w+/.test(row.source_url || '')) return skip(row);\nconst identity = buildGroupIdentity(row.source_url, row.group_id);","handlingStrategy":"validation","validationCode":"function hasGroupIdentity(row) {\n  const m = String(row.source_url || '').match(/^\\/(?:group|article)\\/([A-Za-z0-9_-]+)\\/?$/) || String(row.source_url || '').replace(/^https:\\/\\/[^/]+/, '').match(/^\\/(?:group|article)\\/([A-Za-z0-9_-]+)\\/?$/);\n  return Boolean(String(row.group_id || '').trim() || (m && m[1]));\n}\nif (!hasGroupIdentity(row)) skipRow(row);","typeGuard":"const hasIdentity = (row) => !!(String(row?.group_id || '').trim() || /\\/(?:group|article)\\/[A-Za-z0-9_-]+\\/?$/.test(String(row?.source_url || '').replace(/^https?:\\/\\/[^/]+/, '')));","tryCatchPattern":"try {\n  const identity = buildGroupIdentity(row.source_url, row.group_id);\n} catch (e) {\n  if (e instanceof CommandExecutionError && e.message.includes('missing group_id')) return skipRow(row);\n  throw e;\n}","preventionTips":["Require either group_id or an article/group source_url at the schema level of your ingestion.","Skip non-article rows (videos, topics, homepages) before identity building.","Watch for upstream field renames (group_id -> item_id) and map them."],"tags":["data-integrity","missing-field","toutiao"],"backgroundTag":"missing-required-identifier","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}