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

171 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { getSelfUid, requireArrayEvaluateResult, unwrapEvaluateResult } from './utils.js';
const DEFAULT_LIMIT = 20;
const MAX_LIMIT = 50;
function parsePositiveInt(value, name, defaultValue) {
const raw = value ?? defaultValue;
const number = Number(raw);
if (!Number.isInteger(number) || number <= 0) {
throw new ArgumentError(`weibo favorites ${name} must be a positive integer`);
}
if (number > MAX_LIMIT) {
throw new ArgumentError(`weibo favorites ${name} must be <= ${MAX_LIMIT}`);
}
return number;
}
function parseFavoriteCard(card, favUrl) {
const raw = String(card?.text ?? '');
const lines = raw.split('\n');
let author = '';
let time = '';
let source = '';
let content = '';
let likes = '0';
let comments = '0';
let reposts = '0';
for (const line of lines) {
const t = line.trim();
if (!t || t === '添加') continue;
if (!time && /\d+小时前|\d+分钟前|\d+秒前|昨天|前天|\d{1,2}:\d{2}/.test(t)) {
time = t;
continue;
}
if (t.startsWith('来自')) {
source = t;
continue;
}
if (content) {
const n = Number.parseInt(t, 10);
if (!Number.isNaN(n) && n > 0 && n < 1_000_000 && t === String(n)) {
if (likes === '0') likes = t;
else if (comments === '0') comments = t;
else if (reposts === '0') reposts = t;
continue;
}
}
if (!author && t.length < 40) {
author = t;
continue;
}
if (!content && author) {
content = t;
continue;
}
if (content) content += ` ${t}`;
}
if (!content || !author) return null;
return {
author,
text: content.substring(0, 300),
time,
source,
likes,
comments,
reposts,
url: card?.url || favUrl,
};
}
function dedupeFavorites(items, favUrl) {
const seen = new Set();
const result = [];
for (const item of items) {
const key = item.url && item.url !== favUrl
? item.url
: `${item.author}\n${item.text}\n${item.time}`;
if (seen.has(key)) continue;
seen.add(key);
result.push(item);
}
return result;
}
cli({
site: 'weibo',
name: 'favorites',
access: 'read',
description: '我的微博收藏列表',
domain: 'weibo.com',
strategy: Strategy.COOKIE,
args: [
{ name: 'limit', type: 'int', default: 20, help: '数量(最多50' },
],
columns: ['author', 'text', 'time', 'source', 'likes', 'comments', 'reposts', 'url'],
func: async (page, kwargs) => {
const limit = parsePositiveInt(kwargs.limit, 'limit', DEFAULT_LIMIT);
await page.goto('https://weibo.com');
await page.wait(2);
const uid = await getSelfUid(page);
const favUrl = 'https://www.weibo.com/u/page/fav/' + uid;
await page.goto(favUrl);
await page.wait(4);
for (let i = 0; i < 3; i++) {
await page.evaluate('() => window.scrollBy(0, 800)');
await page.wait(1);
}
const rawData = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
(() => {
const scrollers = document.querySelectorAll('.wbpro-scroller-item, .vue-recycle-scroller__item-view');
const out = [];
for (const s of scrollers) {
// Use textContent to preserve newlines, then split by \n
const bodyEl = s.querySelector('[class*="_body_"]') || s.querySelector('.wbpro-item-body') || s;
// innerText preserves newlines between block elements (unlike textContent)
const rawText = bodyEl.innerText || s.innerText || '';
let postUrl = '';
const anchors = s.querySelectorAll('a[href]');
for (const a of anchors) {
const m = String(a.href).match(/weibo\\.com\\/(\\d+)\\/([a-zA-Z0-9]+)/);
if (m) { postUrl = 'https://weibo.com/' + m[1] + '/' + m[2]; break; }
}
if (rawText.length > 20) out.push({ text: rawText, url: postUrl });
if (out.length >= ${limit}) break;
}
return out;
})()
`)), 'weibo favorites');
if (rawData.length === 0) {
throw new EmptyResultError('weibo favorites', 'No favorites were visible on the favorites page');
}
const items = rawData
.map(card => parseFavoriteCard(card, favUrl))
.filter(Boolean);
const uniqueItems = dedupeFavorites(items, favUrl);
if (uniqueItems.length === 0) {
throw new CommandExecutionError('Failed to parse visible Weibo favorites');
}
return uniqueItems.slice(0, limit);
},
});
export const __test__ = {
parseFavoriteCard,
parsePositiveInt,
dedupeFavorites,
};