From 72b891ca6ac096644ddefff035bf9eed01e2851a Mon Sep 17 00:00:00 2001 From: FF Date: Wed, 20 May 2026 19:59:33 +0800 Subject: [PATCH] fix(xiaohongshu): support creator center URLs in creator-note-detail - Enhance parseNoteId() to extract noteId from creator.xiaohongshu.com URLs - Update creator-note-detail.js to use parseNoteId() for robust ID extraction - Add comprehensive tests for URL parsing and creator-note-detail command Fixes: creator-note-detail fails when passed a full creator center URL instead of a bare note ID, because the raw URL was used directly in page.goto() causing 404/406 errors. --- clis/xiaohongshu/creator-note-detail.js | 4 +- clis/xiaohongshu/creator-note-detail.test.js | 54 ++++++++++++++++++++ clis/xiaohongshu/note-helpers.js | 10 ++++ clis/xiaohongshu/note-helpers.test.js | 28 ++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 clis/xiaohongshu/note-helpers.test.js diff --git a/clis/xiaohongshu/creator-note-detail.js b/clis/xiaohongshu/creator-note-detail.js index 8b9dcd231..bc0b58df2 100644 --- a/clis/xiaohongshu/creator-note-detail.js +++ b/clis/xiaohongshu/creator-note-detail.js @@ -10,6 +10,7 @@ */ import { cli, Strategy } from '@jackwener/opencli/registry'; import { EmptyResultError } from '@jackwener/opencli/errors'; +import { parseNoteId } from './note-helpers.js'; const NOTE_DETAIL_DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/; const NOTE_DETAIL_METRICS = [ { label: '曝光数', section: '基础数据' }, @@ -334,7 +335,8 @@ cli({ ], columns: ['section', 'metric', 'value', 'extra'], func: async (page, kwargs) => { - const noteId = kwargs['note-id']; + const raw = String(kwargs['note-id'] || ''); + const noteId = parseNoteId(raw); const rows = await fetchCreatorNoteDetailRows(page, noteId); const hasCoreMetric = rows.some((row) => row.section !== '笔记信息' && row.value); if (!hasCoreMetric) { diff --git a/clis/xiaohongshu/creator-note-detail.test.js b/clis/xiaohongshu/creator-note-detail.test.js index 9ce050a16..77d7f95fe 100644 --- a/clis/xiaohongshu/creator-note-detail.test.js +++ b/clis/xiaohongshu/creator-note-detail.test.js @@ -289,6 +289,60 @@ describe('xiaohongshu creator-note-detail', () => { expect(page.wait).toHaveBeenCalledWith(expect.objectContaining({ time: expect.any(Number) })); expect(page.wait.mock.calls.length).toBeGreaterThanOrEqual(4); }); + + it('extracts bare note ID from creator center URL', async () => { + const cmd = getRegistry().get('xiaohongshu/creator-note-detail'); + const page = createPageMock([ + { + title: 'URL测试笔记', + infoText: 'URL测试笔记\n2026-05-20 10:00\n切换笔记', + sections: [ + { + title: '基础数据', + metrics: [ + { label: '曝光数', value: '1000', extra: '粉丝占比 5%' }, + { label: '观看数', value: '500', extra: '粉丝占比 10%' }, + { label: '封面点击率', value: '15%', extra: '粉丝 12%' }, + { label: '平均观看时长', value: '45秒', extra: '粉丝 40秒' }, + { label: '涨粉数', value: '5', extra: '' }, + ], + }, + { + title: '互动数据', + metrics: [ + { label: '点赞数', value: '20', extra: '粉丝占比 30%' }, + { label: '评论数', value: '5', extra: '粉丝占比 20%' }, + { label: '收藏数', value: '8', extra: '粉丝占比 25%' }, + { label: '分享数', value: '2', extra: '粉丝占比 0%' }, + ], + }, + ], + }, + null, + null, + null, + null, + ]); + // 使用 creator center URL 格式 + const creatorUrl = 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=abc123def456'; + const result = await cmd.func(page, { 'note-id': creatorUrl }); + // 验证 page.goto 被调用时使用了提取后的 bare note ID + expect(page.goto.mock.calls[0][0]).toBe('https://creator.xiaohongshu.com/statistics/note-detail?noteId=abc123def456'); + expect(result).toEqual([ + { section: '笔记信息', metric: 'note_id', value: 'abc123def456', extra: '' }, + { section: '笔记信息', metric: 'title', value: 'URL测试笔记', extra: '' }, + { section: '笔记信息', metric: 'published_at', value: '2026-05-20 10:00', extra: '' }, + { section: '基础数据', metric: '曝光数', value: '1000', extra: '粉丝占比 5%' }, + { section: '基础数据', metric: '观看数', value: '500', extra: '粉丝占比 10%' }, + { section: '基础数据', metric: '封面点击率', value: '15%', extra: '粉丝 12%' }, + { section: '基础数据', metric: '平均观看时长', value: '45秒', extra: '粉丝 40秒' }, + { section: '基础数据', metric: '涨粉数', value: '5', extra: '' }, + { section: '互动数据', metric: '点赞数', value: '20', extra: '粉丝占比 30%' }, + { section: '互动数据', metric: '评论数', value: '5', extra: '粉丝占比 20%' }, + { section: '互动数据', metric: '收藏数', value: '8', extra: '粉丝占比 25%' }, + { section: '互动数据', metric: '分享数', value: '2', extra: '粉丝占比 0%' }, + ]); + }); it('throws EmptyResultError when the detail page exposes no metrics', async () => { const cmd = getRegistry().get('xiaohongshu/creator-note-detail'); const page = createPageMock(undefined); diff --git a/clis/xiaohongshu/note-helpers.js b/clis/xiaohongshu/note-helpers.js index 78618935a..fa30f9623 100644 --- a/clis/xiaohongshu/note-helpers.js +++ b/clis/xiaohongshu/note-helpers.js @@ -4,6 +4,16 @@ import { ArgumentError } from '@jackwener/opencli/errors'; /** Extract a bare note ID from a full URL or raw ID string. */ export function parseNoteId(input) { const trimmed = input.trim(); + // 支持 creator.xiaohongshu.com URL 格式: ?noteId=xxx + if (trimmed.includes('creator.xiaohongshu.com')) { + try { + const url = new URL(trimmed); + const noteId = url.searchParams.get('noteId'); + if (noteId) return noteId; + } catch { + // 如果 URL 解析失败,回退到正则匹配 + } + } const match = trimmed.match(/\/(?:explore|note|search_result|discovery\/item)\/([a-f0-9]+)|\/user\/profile\/[^/?#]+\/([a-f0-9]+)/i); return match ? (match[1] || match[2]) : trimmed; } diff --git a/clis/xiaohongshu/note-helpers.test.js b/clis/xiaohongshu/note-helpers.test.js new file mode 100644 index 000000000..9429b2d0b --- /dev/null +++ b/clis/xiaohongshu/note-helpers.test.js @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { parseNoteId } from './note-helpers.js'; + +describe('parseNoteId', () => { + it('extracts note ID from standard explore URL', () => { + expect(parseNoteId('https://www.xiaohongshu.com/explore/abc123def456')).toBe('abc123def456'); + }); + + it('extracts note ID from note URL', () => { + expect(parseNoteId('https://www.xiaohongshu.com/note/abc123def456')).toBe('abc123def456'); + }); + + it('extracts note ID from user profile URL', () => { + expect(parseNoteId('https://www.xiaohongshu.com/user/profile/username/abc123def456')).toBe('abc123def456'); + }); + + it('extracts note ID from creator center URL', () => { + expect(parseNoteId('https://creator.xiaohongshu.com/statistics/note-detail?noteId=abc123def456')).toBe('abc123def456'); + }); + + it('returns bare note ID unchanged', () => { + expect(parseNoteId('abc123def456')).toBe('abc123def456'); + }); + + it('handles creator URL with additional query params', () => { + expect(parseNoteId('https://creator.xiaohongshu.com/statistics/note-detail?noteId=abc123&tab=1')).toBe('abc123'); + }); +});