Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

62 lines
3.1 KiB
TypeScript

import { cli, Strategy } from '@jackwener/opencli/registry';
cli({
site: 'test-xhs',
name: 'note-comments',
description: '小红书笔记详情 + 评论(多步合并输出)',
domain: 'www.xiaohongshu.com',
strategy: Strategy.COOKIE,
browser: true,
args: [
{ name: 'id', type: 'string', default: '6745a82f000000000800b6ed', positional: true, help: 'Note ID' },
{ name: 'limit', type: 'int', default: 5, help: 'Max comments' },
],
columns: ['section', 'title', 'author', 'likes', 'text'],
func: async (page, kwargs) => {
const noteId = kwargs.id ?? '6745a82f000000000800b6ed';
const commentLimit = kwargs.limit ?? 5;
// Step 1: Navigate to note detail page
await page.goto('https://www.xiaohongshu.com/explore/' + noteId);
await page.wait(3);
// Step 2: Extract note metadata (title, author, likes)
const meta = await page.evaluate(`(function() {
return {
title: (document.querySelector('#detail-title') || document.querySelector('.title') || {}).textContent?.trim() || '',
author: (document.querySelector('.author-container .username') || document.querySelector('.user-nickname') || {}).textContent?.trim() || '',
likes: (document.querySelector('[data-type="like"] .count') || document.querySelector('.like-wrapper .count') || {}).textContent?.trim() || '0',
};
})()`) as any;
// Step 3: Scroll the note container to trigger comment loading
for (let i = 0; i < 3; i++) {
await page.evaluate(`(function() {
var scroller = document.querySelector('.note-scroller') || document.querySelector('.container');
if (scroller && scroller.scrollTo) { scroller.scrollTo(0, 99999); } else { window.scrollTo(0, document.body.scrollHeight); }
})()`);
await page.wait(1);
}
// Step 4: Extract comments from DOM
const comments = await page.evaluate(`(function() {
var results = [];
var commentEls = document.querySelectorAll('.parent-comment, .comment-item-root');
commentEls.forEach(function(el) {
var item = el.querySelector('.comment-item') || el.querySelector('.comment-inner');
if (!item) return;
var authorEl = item.querySelector('.author-wrapper .name') || item.querySelector('.user-name');
var textEl = item.querySelector('.content') || item.querySelector('.note-text');
var likesEl = item.querySelector('.count');
var author = (authorEl ? authorEl.textContent || '' : '').trim();
var text = (textEl ? textEl.textContent || '' : '').replace(/\\s+/g, ' ').trim();
var likes = (likesEl ? likesEl.textContent || '0' : '0').trim();
if (text) results.push({ author: author, text: text.slice(0, 80), likes: likes });
});
return results;
})()`) as any[];
// Step 5: Merge note meta + comments into unified output
const rows: any[] = [{ section: 'note', title: meta.title, author: meta.author, likes: meta.likes, text: '' }];
for (const c of (comments || []).slice(0, commentLimit)) {
rows.push({ section: 'comment', title: '', author: c.author, likes: c.likes, text: c.text });
}
return rows;
},
});