项目文件夹

文件
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

200 行
7.3 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { log } from '@jackwener/opencli/logger';
import { stripHtml } from './text.js';
function validatePositiveInt(value, name) {
const n = Number(value);
if (!Number.isInteger(n) || n <= 0) {
throw new ArgumentError(`zhihu collection --${name} must be a positive integer`, 'Example: opencli zhihu collection 83283292 --limit 20');
}
return n;
}
function validateNonNegativeInt(value, name) {
const n = Number(value);
if (!Number.isInteger(n) || n < 0) {
throw new ArgumentError(`zhihu collection --${name} must be a non-negative integer`, 'Example: opencli zhihu collection 83283292 --offset 0');
}
return n;
}
async function fetchCollectionPage(page, collectionId, offset, limit) {
const url = `https://www.zhihu.com/api/v4/collections/${collectionId}/items?offset=${offset}&limit=${limit}`;
const data = await page.evaluate(`
(async () => {
const r = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
if (!r.ok) return { __httpError: r.status };
return await r.json();
})()
`);
if (!data || data.__httpError) {
const status = data?.__httpError;
if (status === 401 || status === 403) {
throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch collection data from Zhihu. Please ensure you are logged in.');
}
throw new CommandExecutionError(
status ? `Zhihu collection request failed (HTTP ${status})` : 'Zhihu collection request failed',
'Try again later or rerun with -v for more detail',
);
}
return data;
}
function itemKey(item) {
const content = item?.content || {};
return `${content.type || ''}:${content.id || content.url || JSON.stringify(content).slice(0, 80)}`;
}
function mapCollectionItem(item, rank) {
const content = item.content || {};
const type = content.type || '';
if (!['answer', 'article', 'pin'].includes(type)) {
throw new CommandExecutionError(
`Zhihu collection returned unsupported content type: ${type || 'missing'}`,
'Collection items require a supported content.type so the row identity, title, and URL are not silently blank.',
);
}
let title = '';
let excerpt = '';
let url = '';
let author = '';
let votes = 0;
if (type === 'answer') {
const question = content.question || {};
title = question.title || '';
excerpt = stripHtml(content.content || '').substring(0, 150);
url = content.url || `https://www.zhihu.com/question/${question.id}/answer/${content.id}`;
author = content.author?.name || '匿名用户';
votes = content.voteup_count || 0;
} else if (type === 'article') {
title = content.title || '';
excerpt = stripHtml(content.content || '').substring(0, 150);
url = content.url || `https://zhuanlan.zhihu.com/p/${content.id}`;
author = content.author?.name || '匿名用户';
votes = content.voteup_count || 0;
} else if (type === 'pin') {
title = '想法';
excerpt = stripHtml((content.content || []).map((c) => c.content || '').join(' ')).substring(0, 150);
url = content.url || `https://www.zhihu.com/pin/${content.id}`;
author = content.author?.name || '匿名用户';
votes = content.reaction_count || 0;
}
if (!String(title || '').trim() || !String(url || '').trim() || url.includes('undefined')) {
throw new CommandExecutionError(
'Zhihu collection returned a malformed item without title or URL identity',
'Collection item rows require type, title, and URL so malformed payloads do not become blank listing rows.',
);
}
return {
rank,
type,
title: stripHtml(title).substring(0, 100),
author,
votes,
excerpt,
url,
};
}
cli({
site: 'zhihu',
name: 'collection',
access: 'read',
description: '知乎收藏夹内容列表(需要登录)',
domain: 'www.zhihu.com',
strategy: Strategy.COOKIE,
browser: true,
args: [
{ name: 'id', positional: true, required: true, help: '收藏夹 ID (数字,可从收藏夹 URL 中获取)' },
{ name: 'offset', type: 'int', default: 0, help: '起始偏移量(用于分页)' },
{ name: 'limit', type: 'int', default: 20, help: '每页数量(最大 20' },
],
columns: ['rank', 'type', 'title', 'author', 'votes', 'excerpt', 'url'],
func: async (page, kwargs) => {
const { id, offset = 0, limit = 20 } = kwargs;
const collectionId = String(id);
// 验证收藏夹 ID 为数字
if (!/^\d+$/.test(collectionId)) {
throw new ArgumentError('Collection ID must be numeric', 'Example: opencli zhihu collection 83283292');
}
const pageOffset = validateNonNegativeInt(offset, 'offset');
const requestedLimit = validatePositiveInt(limit, 'limit');
const pageLimit = Math.min(requestedLimit, 20); // 知乎 API 限制每页最大 20
// 先访问知乎主页建立 session
await page.goto('https://www.zhihu.com');
const collected = [];
const seen = new Set();
let totals = 0;
let nextOffset = pageOffset;
const maxPages = Math.ceil(requestedLimit / pageLimit) + 2;
for (let pageIndex = 0; pageIndex < maxPages && collected.length < requestedLimit; pageIndex += 1) {
const currentFetchLimit = Math.min(pageLimit, requestedLimit - collected.length);
const data = await fetchCollectionPage(page, collectionId, nextOffset, currentFetchLimit);
const items = Array.isArray(data.data) ? data.data : [];
const paging = data.paging || {};
totals = Number(paging.totals || totals || 0);
for (const item of items) {
const key = itemKey(item);
if (!seen.has(key)) {
seen.add(key);
collected.push(item);
}
if (collected.length >= requestedLimit) break;
}
if (items.length === 0 || paging.is_end || collected.length >= requestedLimit) break;
if (typeof paging.next === 'string') {
try {
const nextUrl = new URL(paging.next);
const parsedOffset = Number(nextUrl.searchParams.get('offset'));
if (Number.isInteger(parsedOffset) && parsedOffset > nextOffset) {
nextOffset = parsedOffset;
continue;
}
} catch {}
}
if (items.length < currentFetchLimit) break;
const fallbackOffset = nextOffset + items.length;
if (fallbackOffset <= nextOffset) break;
nextOffset = fallbackOffset;
if (totals && nextOffset >= totals) break;
}
// 计算总页数
const totalPages = Math.ceil(totals / pageLimit);
const currentPage = Math.floor(pageOffset / pageLimit) + 1;
// 输出统计信息
if (totals > 0) {
log.info(`收藏夹共有 ${totals} 条内容,共 ${totalPages} 页`);
log.info(`当前第 ${currentPage} 页,显示第 ${pageOffset + 1} - ${Math.min(pageOffset + collected.length, totals)} 条`);
}
if (collected.length === 0) {
throw new EmptyResultError('zhihu collection', `No items found for collection ${collectionId}. The collection may be empty, private, or the offset may be out of range.`);
}
return collected.slice(0, requestedLimit).map((item, i) => mapCollectionItem(item, pageOffset + i + 1));
},
});
export const __test__ = {
stripHtml,
validatePositiveInt,
validateNonNegativeInt,
itemKey,
mapCollectionItem,
};