Files
wehub-resource-sync 9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

658 lines
26 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.
/**
* Douban adapter utilities.
*/
import { ArgumentError, CliError, EmptyResultError } from '@jackwener/opencli/errors';
import { clamp } from '../_shared/common.js';
const DOUBAN_PHOTO_PAGE_SIZE = 30;
const MAX_DOUBAN_PHOTOS = 500;
const clampLimit = (limit) => clamp(limit || 20, 1, 50);
const clampPhotoLimit = (limit) => clamp(limit || 120, 1, MAX_DOUBAN_PHOTOS);
const DOUBAN_SEARCH_READY_SELECTOR = '.item-root .title-text, .item-root .title a, .result-list .result-item h3 a';
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
function firstNonEmpty(values) {
for (const value of values) {
const normalized = normalizeText(value);
if (normalized)
return normalized;
}
return '';
}
function splitDoubanPeople(value) {
return normalizeText(value)
.split(/\s*\/\s*/)
.map((entry) => normalizeText(entry))
.filter(Boolean);
}
function parseDoubanBookInfoText(infoText) {
const lines = String(infoText || '')
.replace(/\r/g, '\n')
.split('\n')
.map((line) => normalizeText(line))
.filter(Boolean);
const map = {};
for (const line of lines) {
const match = line.match(/^([^:]+)\s*[:]\s*(.*)$/);
if (!match)
continue;
const label = normalizeText(match[1]);
const value = normalizeText(match[2]);
if (!label)
continue;
map[label] = value;
}
return map;
}
function parseDoubanRating(value) {
const normalized = normalizeText(value);
if (!normalized)
return 0;
const parsed = Number.parseFloat(normalized);
return Number.isFinite(parsed) ? parsed : 0;
}
function parseDoubanCount(value) {
const normalized = normalizeText(value).replace(/[^\d]/g, '');
if (!normalized)
return 0;
const parsed = Number.parseInt(normalized, 10);
return Number.isFinite(parsed) ? parsed : 0;
}
function parseDoubanPageCount(value) {
const match = normalizeText(value).match(/(\d+)/);
if (!match)
return null;
const parsed = Number.parseInt(match[1], 10);
return Number.isFinite(parsed) ? parsed : null;
}
function extractDoubanPublishYear(value) {
const match = normalizeText(value).match(/\b(19|20)\d{2}\b/);
return match?.[0] || '';
}
export function splitDoubanTitle(fullTitle) {
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
const normalized = normalizeText(fullTitle);
if (!normalized)
return { title: '', originalTitle: '' };
const match = normalized.match(/^([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+(?:\s*[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef·::!?]+)*)\s+(.+)$/);
if (!match) {
return { title: normalized, originalTitle: '' };
}
return {
title: normalizeText(match[1]),
originalTitle: normalizeText(match[2]),
};
}
async function ensureDoubanReady(page) {
const state = await page.evaluate(`
(() => {
const title = (document.title || '').trim();
const href = (location.href || '').trim();
const blocked = href.includes('sec.douban.com') || /登录跳转/.test(title) || /异常请求/.test(document.body?.innerText || '');
return { blocked, title, href };
})()
`);
if (state?.blocked) {
throw new CliError('AUTH_REQUIRED', 'Douban requires a logged-in browser session before these commands can load data.', 'Please sign in to douban.com in the browser that opencli reuses, then rerun the command.');
}
}
function isDetachedPageError(error) {
const message = error instanceof Error ? error.message : String(error || '');
return /Detached while handling command|Debugger is not attached to the tab|Target closed|No tab with id/i.test(message);
}
async function withDetachedRetry(task, options = {}) {
const attempts = Math.max(1, options.attempts || 2);
let lastError;
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
return await task();
}
catch (error) {
lastError = error;
if (attempt >= attempts - 1 || !isDetachedPageError(error)) {
throw error;
}
}
}
throw lastError;
}
function buildDoubanSearchUrl(type, keyword) {
const url = new URL(`https://search.douban.com/${encodeURIComponent(type)}/subject_search`);
url.searchParams.set('search_text', String(keyword || ''));
if (String(type || '').trim() === 'book') {
url.searchParams.set('cat', '1001');
}
return url.toString();
}
export function normalizeDoubanSubjectId(subjectId) {
const normalized = String(subjectId || '').trim();
if (!/^\d+$/.test(normalized)) {
throw new ArgumentError(`Invalid Douban subject ID: ${subjectId}`);
}
return normalized;
}
export function promoteDoubanPhotoUrl(url, size = 'l') {
const normalized = String(url || '').trim();
if (!normalized)
return '';
if (/^[a-z]+:/i.test(normalized) && !/^https?:/i.test(normalized))
return '';
return normalized.replace(/\/view\/photo\/[^/]+\/public\//, `/view/photo/${size}/public/`);
}
export function resolveDoubanPhotoAssetUrl(candidates, baseUrl = '') {
for (const candidate of candidates) {
const normalized = String(candidate || '').trim();
if (!normalized)
continue;
let resolved = normalized;
try {
resolved = baseUrl
? new URL(normalized, baseUrl).toString()
: new URL(normalized).toString();
}
catch {
resolved = normalized;
}
if (/^https?:\/\//i.test(resolved)) {
return resolved;
}
}
return '';
}
export function getDoubanPhotoExtension(url) {
const normalized = String(url || '').trim();
if (!normalized)
return '.jpg';
try {
const ext = new URL(normalized).pathname.match(/\.(jpe?g|png|gif|webp|avif|bmp)$/i)?.[0];
return ext || '.jpg';
}
catch {
const ext = normalized.match(/\.(jpe?g|png|gif|webp|avif|bmp)(?:$|[?#])/i)?.[0];
return ext ? ext.replace(/[?#].*$/, '') : '.jpg';
}
}
export function normalizeDoubanBookSubject(raw) {
const info = parseDoubanBookInfoText(raw?.infoText);
const title = firstNonEmpty([raw?.title]);
const subtitle = firstNonEmpty([raw?.subtitle, info['副标题']]);
const originalTitle = firstNonEmpty([raw?.originalTitle, info['原作名']]);
const authors = splitDoubanPeople(firstNonEmpty([info['作者']]));
const translators = splitDoubanPeople(firstNonEmpty([info['译者']]));
const publisher = firstNonEmpty([info['出版社'], info['出品方']]);
const publishDate = firstNonEmpty([info['出版年']]);
const publishYear = extractDoubanPublishYear(publishDate);
const pageCount = parseDoubanPageCount(info['页数']);
const binding = firstNonEmpty([info['装帧']]);
const price = firstNonEmpty([info['定价']]);
const series = firstNonEmpty([info['丛书']]);
const isbnRaw = firstNonEmpty([info['ISBN']]).replace(/[^\dxX]/g, '');
const isbn10 = isbnRaw.length === 10 ? isbnRaw : '';
const isbn13 = isbnRaw.length === 13 ? isbnRaw : '';
return {
id: normalizeDoubanSubjectId(raw?.id),
type: 'book',
title,
subtitle,
originalTitle,
authors,
translators,
publisher,
publishDate,
publishYear,
pageCount,
binding,
price,
series,
isbn10,
isbn13,
rating: parseDoubanRating(raw?.rating),
ratingCount: parseDoubanCount(raw?.ratingCount),
summary: normalizeText(raw?.summary),
cover: firstNonEmpty([raw?.cover]),
url: firstNonEmpty([raw?.url]),
};
}
async function loadDoubanMovieSubject(page, subjectId) {
const normalizedId = normalizeDoubanSubjectId(subjectId);
const data = await withDetachedRetry(async () => {
await page.goto(`https://movie.douban.com/subject/${normalizedId}/`, { waitUntil: 'load', settleMs: 1500 });
await ensureDoubanReady(page);
await page.wait({ selector: 'span[property="v:itemreviewed"], #info', timeout: 8 }).catch(() => { });
return page.evaluate(`
(() => {
const id = ${JSON.stringify(normalizedId)};
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const { title, originalTitle } = (${splitDoubanTitle.toString()})(normalize(document.querySelector('span[property="v:itemreviewed"]')?.textContent || ''));
const year = normalize(document.querySelector('.year')?.textContent).replace(/[()()]/g, '');
const rating = parseFloat(normalize(document.querySelector('strong[property="v:average"]')?.textContent || '0')) || 0;
const ratingCount = parseInt(normalize(document.querySelector('span[property="v:votes"]')?.textContent || '0'), 10) || 0;
const genres = Array.from(document.querySelectorAll('span[property="v:genre"]'))
.map((node) => normalize(node.textContent))
.filter(Boolean)
.join(',');
const directors = Array.from(document.querySelectorAll('a[rel="v:directedBy"]'))
.map((node) => normalize(node.textContent))
.filter(Boolean)
.join(',');
const casts = Array.from(document.querySelectorAll('a[rel="v:starring"]'))
.slice(0, 5)
.map((node) => normalize(node.textContent))
.filter(Boolean);
const infoText = document.querySelector('#info')?.textContent || '';
let country = [];
const countryMatch = infoText.match(/制片国家\\/地区:\\s*([^\\n]+)/);
if (countryMatch) {
country = countryMatch[1].trim().split(/\\s*\\/\\s*/).filter(Boolean);
}
const durationRaw = normalize(document.querySelector('span[property="v:runtime"]')?.textContent || '');
const durationMatch = durationRaw.match(/(\\d+)/);
const summary = normalize(document.querySelector('span[property="v:summary"]')?.textContent || '');
return {
id,
type: 'movie',
title,
originalTitle,
year,
rating,
ratingCount,
genres,
directors,
casts,
country,
duration: durationMatch ? parseInt(durationMatch[1], 10) : null,
summary: summary.slice(0, 200),
url: 'https://movie.douban.com/subject/' + id + '/',
};
})()
`);
});
return data;
}
async function loadDoubanBookSubject(page, subjectId) {
const normalizedId = normalizeDoubanSubjectId(subjectId);
const data = await withDetachedRetry(async () => {
await page.goto(`https://book.douban.com/subject/${normalizedId}/`, { waitUntil: 'load', settleMs: 1500 });
await ensureDoubanReady(page);
await page.wait({ selector: 'h1 span, #info', timeout: 8 }).catch(() => { });
return page.evaluate(`
(() => {
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const pickSummary = () => {
const nodes = Array.from(document.querySelectorAll('#link-report .intro, .related_info .intro'));
for (let i = nodes.length - 1; i >= 0; i -= 1) {
const text = normalize(nodes[i]?.textContent);
if (text) return text;
}
return '';
};
return {
id: ${JSON.stringify(normalizedId)},
title: normalize(document.querySelector('h1 span')?.textContent || document.querySelector('h1')?.textContent || ''),
subtitle: '',
originalTitle: '',
infoText: document.querySelector('#info')?.innerText || document.querySelector('#info')?.textContent || '',
rating: normalize(document.querySelector('strong.rating_num, strong[property="v:average"]')?.textContent || ''),
ratingCount: normalize(document.querySelector('a.rating_people > span, span[property="v:votes"]')?.textContent || ''),
summary: pickSummary(),
cover: document.querySelector('#mainpic img')?.getAttribute('src') || '',
url: location.href,
};
})()
`);
});
return normalizeDoubanBookSubject(data);
}
export async function loadDoubanSubjectDetail(page, subjectId, subjectType = 'movie') {
const type = String(subjectType || 'movie').trim() === 'book' ? 'book' : 'movie';
if (type === 'book') {
return loadDoubanBookSubject(page, subjectId);
}
return loadDoubanMovieSubject(page, subjectId);
}
export async function loadDoubanSubjectPhotos(page, subjectId, options = {}) {
const normalizedId = normalizeDoubanSubjectId(subjectId);
const type = String(options.type || 'Rb').trim() || 'Rb';
const targetPhotoId = String(options.targetPhotoId || '').trim();
const safeLimit = targetPhotoId ? Number.MAX_SAFE_INTEGER : clampPhotoLimit(Number(options.limit) || 120);
const resolvePhotoAssetUrlSource = resolveDoubanPhotoAssetUrl.toString();
const galleryUrl = `https://movie.douban.com/subject/${normalizedId}/photos?type=${encodeURIComponent(type)}`;
await page.goto(galleryUrl);
await page.wait(2);
await ensureDoubanReady(page);
const data = await page.evaluate(`
(async () => {
const subjectId = ${JSON.stringify(normalizedId)};
const type = ${JSON.stringify(type)};
const limit = ${safeLimit};
const targetPhotoId = ${JSON.stringify(targetPhotoId)};
const pageSize = ${DOUBAN_PHOTO_PAGE_SIZE};
const resolveDoubanPhotoAssetUrl = ${resolvePhotoAssetUrlSource};
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const toAbsoluteUrl = (value) => {
if (!value) return '';
try {
return new URL(value, location.origin).toString();
} catch {
return value;
}
};
const promotePhotoUrl = (value) => {
const absolute = toAbsoluteUrl(value);
if (!absolute) return '';
if (/^[a-z]+:/i.test(absolute) && !/^https?:/i.test(absolute)) return '';
return absolute.replace(/\\/view\\/photo\\/[^/]+\\/public\\//, '/view/photo/l/public/');
};
const buildPageUrl = (start) => {
const url = new URL(location.href);
url.searchParams.set('type', type);
if (start > 0) url.searchParams.set('start', String(start));
else url.searchParams.delete('start');
return url.toString();
};
const getTitle = (doc) => {
const raw = normalize(doc.querySelector('#content h1')?.textContent)
|| normalize(doc.querySelector('title')?.textContent);
return raw.replace(/\\s*\\(豆瓣\\)\\s*$/, '');
};
const extractPhotos = (doc, pageNumber) => {
const nodes = Array.from(doc.querySelectorAll('.poster-col3 li, .poster-col3l li, .article li'));
const rows = [];
for (const node of nodes) {
const link = node.querySelector('a[href*="/photos/photo/"]');
const img = node.querySelector('img');
if (!link || !img) continue;
const detailUrl = toAbsoluteUrl(link.getAttribute('href') || '');
const photoId = detailUrl.match(/\\/photo\\/(\\d+)/)?.[1] || '';
const thumbUrl = resolveDoubanPhotoAssetUrl([
img.getAttribute('data-origin'),
img.getAttribute('data-src'),
img.getAttribute('src'),
], location.href);
const imageUrl = promotePhotoUrl(thumbUrl);
const title = normalize(link.getAttribute('title'))
|| normalize(img.getAttribute('alt'))
|| (photoId ? 'photo_' + photoId : 'photo_' + String(rows.length + 1));
if (!detailUrl || !thumbUrl || !imageUrl) continue;
rows.push({
photoId,
title,
imageUrl,
thumbUrl,
detailUrl,
page: pageNumber,
});
}
return rows;
};
const subjectTitle = getTitle(document);
const seen = new Set();
const photos = [];
for (let pageIndex = 0; photos.length < limit; pageIndex += 1) {
let doc = document;
if (pageIndex > 0) {
const response = await fetch(buildPageUrl(pageIndex * pageSize), { credentials: 'include' });
if (!response.ok) break;
const html = await response.text();
doc = new DOMParser().parseFromString(html, 'text/html');
}
const pagePhotos = extractPhotos(doc, pageIndex + 1);
if (!pagePhotos.length) break;
let appended = 0;
let foundTarget = false;
for (const photo of pagePhotos) {
const key = photo.photoId || photo.detailUrl || photo.imageUrl;
if (seen.has(key)) continue;
seen.add(key);
photos.push({
index: photos.length + 1,
...photo,
});
appended += 1;
if (targetPhotoId && photo.photoId === targetPhotoId) {
foundTarget = true;
break;
}
if (photos.length >= limit) break;
}
if (foundTarget || pagePhotos.length < pageSize || appended === 0) break;
}
return {
subjectId,
subjectTitle,
type,
photos,
};
})()
`);
const photos = Array.isArray(data?.photos) ? data.photos : [];
if (!photos.length) {
throw new EmptyResultError('douban photos', 'No photos found. Try a different subject ID or a different --type value such as Rb.');
}
return {
subjectId: normalizedId,
subjectTitle: String(data?.subjectTitle || '').trim(),
type,
photos,
};
}
export async function loadDoubanBookHot(page, limit) {
const safeLimit = clampLimit(limit);
await page.goto('https://book.douban.com/chart');
await page.wait(4);
await ensureDoubanReady(page);
const data = await page.evaluate(`
(() => {
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const books = [];
for (const el of Array.from(document.querySelectorAll('.media.clearfix'))) {
try {
const titleEl = el.querySelector('h2 a[href*="/subject/"]');
const title = normalize(titleEl?.textContent);
let url = titleEl?.getAttribute('href') || '';
if (!title || !url) continue;
if (!url.startsWith('http')) url = 'https://book.douban.com' + url;
const info = normalize(el.querySelector('.subject-abstract, .pl, .pub')?.textContent);
const infoParts = info.split('/').map((part) => part.trim()).filter(Boolean);
const ratingText = normalize(el.querySelector('.subject-rating .font-small, .rating_nums, .rating')?.textContent);
const quote = Array.from(el.querySelectorAll('.subject-tags .tag'))
.map((node) => normalize(node.textContent))
.filter(Boolean)
.join(' / ');
books.push({
rank: parseInt(normalize(el.querySelector('.green-num-box')?.textContent), 10) || books.length + 1,
title,
rating: parseFloat(ratingText) || 0,
quote,
author: infoParts[0] || '',
publisher: infoParts.find((part) => /出版社|出版公司|Press/i.test(part)) || infoParts[2] || '',
year: infoParts.find((part) => /\\d{4}(?:-\\d{1,2})?/.test(part))?.match(/\\d{4}/)?.[0] || '',
price: infoParts.find((part) => /元|USD|\\$|¥/.test(part)) || '',
url,
cover: el.querySelector('img')?.getAttribute('src') || '',
});
} catch {}
}
return books.slice(0, ${safeLimit});
})()
`);
return Array.isArray(data) ? data : [];
}
export async function loadDoubanMovieHot(page, limit) {
const safeLimit = clampLimit(limit);
await page.goto('https://movie.douban.com/chart');
await page.wait(4);
await ensureDoubanReady(page);
const data = await page.evaluate(`
(() => {
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const results = [];
for (const el of Array.from(document.querySelectorAll('.item'))) {
const titleEl = el.querySelector('.pl2 a');
const title = normalize(titleEl?.textContent);
let url = titleEl?.getAttribute('href') || '';
if (!title || !url) continue;
if (!url.startsWith('http')) url = 'https://movie.douban.com' + url;
const id = url.match(/subject\\/(\\d+)/)?.[1] || '';
const info = normalize(el.querySelector('.pl2 p')?.textContent);
const yearMatch = info.match(/\\b(19|20)\\d{2}\\b/);
const votesText = normalize(el.querySelector('.star .pl')?.textContent);
const votes = parseInt(votesText.replace(/[^0-9]/g, ''), 10) || 0;
results.push({
rank: results.length + 1,
id,
title,
rating: parseFloat(normalize(el.querySelector('.rating_nums')?.textContent)) || 0,
votes,
year: yearMatch?.[0] || '',
url,
cover: el.querySelector('img')?.getAttribute('src') || '',
});
if (results.length >= ${safeLimit}) break;
}
return results;
})()
`);
const results = Array.isArray(data) ? data : [];
if (!results.length) {
throw new EmptyResultError('douban movie-hot', 'No movie chart rows were parsed from movie.douban.com/chart.');
}
return results;
}
export function inferDoubanSearchResultType(searchType, item = {}) {
const fallbackType = String(searchType || '').trim() || 'movie';
if (fallbackType !== 'movie') {
return fallbackType;
}
const moreUrl = String(item.moreUrl || item.more_url || '').trim();
const isTv = moreUrl.match(/is_tv:\s*['"]?([01])['"]?/)?.[1] || '';
if (isTv === '1') {
return 'tvshow';
}
const labels = Array.isArray(item.labels)
? item.labels
.map((label) => typeof label === 'string' ? label.trim() : String(label?.text || '').trim())
.filter(Boolean)
: [];
return labels.includes('剧集') ? 'tvshow' : fallbackType;
}
export async function searchDouban(page, type, keyword, limit) {
const safeLimit = clampLimit(limit);
const inferDoubanSearchResultTypeSource = inferDoubanSearchResultType.toString();
const searchUrl = buildDoubanSearchUrl(type, keyword);
const data = await withDetachedRetry(async () => {
await page.goto(searchUrl, { waitUntil: 'load', settleMs: 1500 });
await ensureDoubanReady(page);
await page.wait({ selector: DOUBAN_SEARCH_READY_SELECTOR, timeout: 8 }).catch(() => { });
return page.evaluate(`
(async () => {
const type = ${JSON.stringify(type)};
const inferDoubanSearchResultType = ${inferDoubanSearchResultTypeSource};
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const seen = new Set();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const rawItems = Array.isArray(window.__DATA__?.items) ? window.__DATA__.items : [];
const rawItemsById = new Map(
rawItems
.map((item) => [String(item?.id || '').trim(), item])
.filter(([id]) => id),
);
for (let i = 0; i < 20; i += 1) {
if (document.querySelector('.item-root .title-text, .item-root .title a')) break;
await sleep(300);
}
const items = Array.from(document.querySelectorAll('.item-root, .result-list .result-item'));
const results = [];
for (const el of items) {
const titleEl = el.querySelector('.title-text, .title a, .title h3 a, h3 a, a[title]');
const title = normalize(titleEl?.textContent) || normalize(titleEl?.getAttribute('title'));
let url = titleEl?.getAttribute('href') || el.querySelector('a[href*="/subject/"]')?.getAttribute('href') || '';
if (!title || !url) continue;
if (!url.startsWith('http')) url = 'https://search.douban.com' + url;
if (!url.includes('/subject/') || seen.has(url)) continue;
seen.add(url);
const id = url.match(/subject\\/(\\d+)/)?.[1] || '';
const rawItem = rawItemsById.get(id) || {};
const ratingText = normalize(el.querySelector('.rating_nums')?.textContent);
const abstract = normalize(
el.querySelector('.meta.abstract, .meta, .abstract, .subject-abstract, p')?.textContent,
);
results.push({
rank: results.length + 1,
id,
type: inferDoubanSearchResultType(type, rawItem),
title,
rating: ratingText.includes('.') ? parseFloat(ratingText) : 0,
abstract: abstract.slice(0, 100) + (abstract.length > 100 ? '...' : ''),
url,
cover: el.querySelector('img')?.getAttribute('src') || '',
});
if (results.length >= ${safeLimit}) break;
}
return results;
})()
`);
});
return Array.isArray(data) ? data : [];
}
/**
* Get current user's Douban ID from movie.douban.com/mine page
*/
export async function getSelfUid(page) {
await page.goto('https://movie.douban.com/mine');
await page.wait({ time: 2 });
const uid = await page.evaluate(`
(() => {
// 方案1: 尝试从全局变量获取
if (window.__DATA__ && window.__DATA__.uid) {
return window.__DATA__.uid;
}
// 方案2: 从导航栏用户链接获取
const navUserLink = document.querySelector('.nav-user-account a');
if (navUserLink) {
const href = navUserLink.href || '';
const match = href.match(/people\\/([^/]+)/);
if (match) return match[1];
}
// 方案3: 从页面中的个人主页链接获取
const profileLink = document.querySelector('a[href*="/people/"]');
if (profileLink) {
const href = profileLink.getAttribute('href') || profileLink.href || '';
const match = href.match(/people\\/([^/]+)/);
if (match) return match[1];
}
// 方案4: 从头部用户名区域获取
const userLink = document.querySelector('.global-nav-items a[href*="/people/"]');
if (userLink) {
const href = userLink.getAttribute('href') || userLink.href || '';
const match = href.match(/people\\/([^/]+)/);
if (match) return match[1];
}
return '';
})()
`);
if (!uid) {
throw new Error('Not logged in to Douban. Please login in Chrome first.');
}
return uid;
}