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

108 lines
4.3 KiB
JavaScript

import { cli, Strategy } from '@jackwener/opencli/registry';
import {
callGateway,
emptyResult,
formatRating,
makeDeepLink,
requireBookId,
requirePositiveInt,
truncate,
} from './utils.js';
/**
* Two endpoints share a single command surface:
* - no bookId → `/book/recommend` (personalized "for you" feed)
* - bookId → `/book/similar` (similar-books for a given book)
*
* Pagination differs: `/recommend` uses `searchIdx`; `/similar` uses `idx` plus
* an opaque `sessionId` cursor. The CLI surfaces both verbatim so multi-page
* fetches can use the SKILL contract without translation.
*/
cli({
site: 'weread-official',
name: 'discover',
access: 'read',
description: 'Personalized or similar-book recommendations from WeRead',
domain: 'weread.qq.com',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'bookId', positional: true, help: 'Anchor bookId for similar-book mode; omit for personalized recommendations' },
{ name: 'count', type: 'int', default: 12, help: 'Page size (default 12)' },
{ name: 'max-idx', type: 'int', default: 0, help: 'Pagination cursor (recommend: previous searchIdx; similar: previous idx)' },
{ name: 'session-id', help: 'Carry-forward sessionId for /book/similar paging' },
],
columns: ['rank', 'mode', 'bookId', 'title', 'author', 'rating', 'readingCount', 'category', 'idx', 'reason', 'cover', 'intro', 'link'],
func: async (args) => {
const count = requirePositiveInt(args.count, 'count', { defaultValue: 12, max: 50 });
const maxIdx = Number(args['max-idx'] ?? 0);
const rawBookId = args.bookId !== undefined && args.bookId !== null && String(args.bookId).trim() !== ''
? requireBookId(args.bookId)
: null;
if (!rawBookId) {
return runRecommend({ count, maxIdx });
}
return runSimilar({ bookId: rawBookId, count, maxIdx, sessionId: args['session-id'] });
},
});
async function runRecommend({ count, maxIdx }) {
const payload = await callGateway('/book/recommend', { count, maxIdx });
const books = Array.isArray(payload?.books) ? payload.books : [];
if (books.length === 0) {
emptyResult('discover', 'No personalized recommendations available.');
}
return books.map((entry, i) => {
const bookId = String(entry?.bookId ?? '').trim();
return {
rank: i + 1,
mode: 'recommend',
bookId,
title: String(entry?.title ?? ''),
author: String(entry?.author ?? ''),
rating: formatRating(entry?.newRating),
readingCount: Number(entry?.readingCount ?? 0),
category: String(entry?.category ?? ''),
idx: Number(entry?.searchIdx ?? 0),
reason: String(entry?.reason ?? ''),
cover: String(entry?.cover ?? ''),
intro: truncate(entry?.intro, 200),
link: bookId ? makeDeepLink({ bookId }) : '',
};
});
}
async function runSimilar({ bookId, count, maxIdx, sessionId }) {
const params = { bookId, count, maxIdx };
const sid = String(sessionId ?? '').trim();
if (sid) params.sessionId = sid;
const payload = await callGateway('/book/similar', params);
const inner = payload?.booksimilar ?? payload;
const books = Array.isArray(inner?.books) ? inner.books : [];
if (books.length === 0) {
emptyResult('discover', `No similar books for bookId=${bookId}.`);
}
return books.map((wrapper, i) => {
// /book/similar nests bookInfo under entry.book.bookInfo
const info = wrapper?.book?.bookInfo ?? wrapper?.bookInfo ?? {};
const id = String(info?.bookId ?? '').trim();
return {
rank: i + 1,
mode: 'similar',
bookId: id,
title: String(info?.title ?? ''),
author: String(info?.author ?? ''),
rating: formatRating(info?.newRating),
readingCount: Number(info?.readingCount ?? 0),
category: String(info?.category ?? ''),
idx: Number(wrapper?.idx ?? 0),
reason: '', // /book/similar does not surface a recommendation reason
cover: String(info?.cover ?? ''),
intro: truncate(info?.intro, 200),
link: id ? makeDeepLink({ bookId: id }) : '',
};
});
}