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
79 lines
3.2 KiB
JavaScript
79 lines
3.2 KiB
JavaScript
function toCleanString(value) {
|
|
return typeof value === 'string' ? value.trim() : value == null ? '' : String(value).trim();
|
|
}
|
|
export function normalizeXhsUserId(input) {
|
|
const trimmed = toCleanString(input);
|
|
const withoutQuery = trimmed.replace(/[?#].*$/, '');
|
|
const matched = withoutQuery.match(/\/user\/profile\/([a-zA-Z0-9]+)/);
|
|
if (matched?.[1])
|
|
return matched[1];
|
|
return withoutQuery.replace(/\/+$/, '').split('/').pop() ?? withoutQuery;
|
|
}
|
|
export function flattenXhsNoteGroups(noteGroups) {
|
|
if (!Array.isArray(noteGroups))
|
|
return [];
|
|
const notes = [];
|
|
for (const group of noteGroups) {
|
|
if (!group)
|
|
continue;
|
|
if (Array.isArray(group)) {
|
|
for (const item of group) {
|
|
if (item)
|
|
notes.push(item);
|
|
}
|
|
continue;
|
|
}
|
|
notes.push(group);
|
|
}
|
|
return notes;
|
|
}
|
|
/**
|
|
* Build a signed user-profile note URL on the given web host (defaults to
|
|
* `www.xiaohongshu.com`). The rednote adapter passes `'www.rednote.com'` so
|
|
* the same builder works for both sites.
|
|
*/
|
|
export function buildXhsNoteUrl(userId, noteId, xsecToken, webHost = 'www.xiaohongshu.com') {
|
|
const cleanUserId = toCleanString(userId);
|
|
const cleanNoteId = toCleanString(noteId);
|
|
if (!cleanUserId || !cleanNoteId)
|
|
return '';
|
|
const url = new URL(`https://${webHost}/user/profile/${cleanUserId}/${cleanNoteId}`);
|
|
const cleanToken = toCleanString(xsecToken);
|
|
if (cleanToken) {
|
|
url.searchParams.set('xsec_token', cleanToken);
|
|
url.searchParams.set('xsec_source', 'pc_user');
|
|
}
|
|
return url.toString();
|
|
}
|
|
/**
|
|
* Normalise a Pinia user-store snapshot into CLI rows. `webHost` is forwarded
|
|
* to `buildXhsNoteUrl` so the resulting URLs point at the calling site.
|
|
*/
|
|
export function extractXhsUserNotes(snapshot, fallbackUserId, webHost = 'www.xiaohongshu.com') {
|
|
const notes = flattenXhsNoteGroups(snapshot.noteGroups);
|
|
const rows = [];
|
|
const seen = new Set();
|
|
for (const entry of notes) {
|
|
const noteCard = entry?.noteCard ?? entry?.note_card ?? entry;
|
|
if (!noteCard || typeof noteCard !== 'object')
|
|
continue;
|
|
const noteId = toCleanString(noteCard.noteId ?? noteCard.note_id ?? entry?.noteId ?? entry?.note_id ?? entry?.id);
|
|
if (!noteId || seen.has(noteId))
|
|
continue;
|
|
seen.add(noteId);
|
|
const userId = toCleanString(noteCard.user?.userId ?? noteCard.user?.user_id ?? fallbackUserId);
|
|
const xsecToken = toCleanString(entry?.xsecToken ?? entry?.xsec_token ?? noteCard.xsecToken ?? noteCard.xsec_token);
|
|
const likes = toCleanString(noteCard.interactInfo?.likedCount ?? noteCard.interact_info?.liked_count ?? 0) || '0';
|
|
const cover = toCleanString(noteCard.cover?.urlDefault ?? noteCard.cover?.urlPre ?? noteCard.cover?.url ?? '');
|
|
rows.push({
|
|
id: noteId,
|
|
title: toCleanString(noteCard.displayTitle ?? noteCard.display_title ?? noteCard.title),
|
|
type: toCleanString(noteCard.type),
|
|
likes,
|
|
cover,
|
|
url: buildXhsNoteUrl(userId || fallbackUserId, noteId, xsecToken, webHost),
|
|
});
|
|
}
|
|
return rows;
|
|
}
|