fix(adapters): surface silent empty fallbacks
Resolve the remaining silent-empty-fallback typed-error baseline entries across Douyin, Jike, and WeRead adapters.
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **adapters** — surface the remaining `silent-empty-fallback` adapter failures as typed errors. Douyin user video comment fetch failures, Jike SSR JSON parse failures, and WeRead search-page fetch failures now throw `CommandExecutionError`; true empty Douyin/Jike/WeRead result sets now throw `EmptyResultError`.
|
||||
|
||||
## [1.7.22](https://github.com/jackwener/opencli/compare/v1.7.21...v1.7.22) (2026-05-15)
|
||||
|
||||
External CLI ergonomics + two adapter envelope/auth fixes. New `longbridge` external CLI entry; `opencli list` / root help now render human-readable brand labels for executables whose bare name is ambiguous.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { CliError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
import { fetchDouyinComments, fetchDouyinUserVideos } from './_shared/public-api.js';
|
||||
export const MAX_USER_VIDEOS_LIMIT = 20;
|
||||
export const USER_VIDEO_COMMENT_CONCURRENCY = 4;
|
||||
@@ -27,8 +28,11 @@ async function fetchTopComments(page, awemeId, count) {
|
||||
try {
|
||||
return await fetchDouyinComments(page, awemeId, count);
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
catch (error) {
|
||||
if (error instanceof CliError) {
|
||||
throw error;
|
||||
}
|
||||
throw new CommandExecutionError(`Failed to fetch Douyin comments for video ${awemeId}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
cli({
|
||||
@@ -53,6 +57,9 @@ cli({
|
||||
await page.goto(`https://www.douyin.com/user/${secUid}`);
|
||||
await page.wait(3);
|
||||
const awemeList = (await fetchDouyinUserVideos(page, secUid, limit)).slice(0, limit);
|
||||
if (awemeList.length === 0) {
|
||||
throw new EmptyResultError('douyin user-videos', `No videos were returned for sec_uid ${secUid}. Confirm the user exists and the Douyin session is valid.`);
|
||||
}
|
||||
const videos = withComments
|
||||
? await mapInBatches(awemeList, USER_VIDEO_COMMENT_CONCURRENCY, async (video) => ({
|
||||
...video,
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock('./_shared/public-api.js', () => ({
|
||||
fetchDouyinComments: fetchDouyinCommentsMock,
|
||||
}));
|
||||
import { getRegistry } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
import { DEFAULT_COMMENT_LIMIT, MAX_USER_VIDEOS_LIMIT, normalizeCommentLimit, normalizeUserVideosLimit } from './user-videos.js';
|
||||
describe('douyin user-videos', () => {
|
||||
beforeEach(() => {
|
||||
@@ -105,4 +106,46 @@ describe('douyin user-videos', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('throws EmptyResultError when the user videos API returns no rows', async () => {
|
||||
const command = [...getRegistry().values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
|
||||
expect(command?.func).toBeDefined();
|
||||
if (!command?.func)
|
||||
throw new Error('douyin user-videos command not registered');
|
||||
fetchDouyinUserVideosMock.mockResolvedValueOnce([]);
|
||||
const page = {
|
||||
goto: vi.fn().mockResolvedValue(undefined),
|
||||
wait: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
await expect(command.func(page, {
|
||||
sec_uid: 'MS4w-empty',
|
||||
limit: 3,
|
||||
with_comments: true,
|
||||
comment_limit: 5,
|
||||
})).rejects.toBeInstanceOf(EmptyResultError);
|
||||
});
|
||||
it('surfaces comment enrichment failures instead of returning empty comments', async () => {
|
||||
const command = [...getRegistry().values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
|
||||
expect(command?.func).toBeDefined();
|
||||
if (!command?.func)
|
||||
throw new Error('douyin user-videos command not registered');
|
||||
fetchDouyinUserVideosMock.mockResolvedValueOnce([
|
||||
{
|
||||
aweme_id: '3',
|
||||
desc: 'comment failure',
|
||||
video: { duration: 2000, play_addr: { url_list: ['https://example.com/fail.mp4'] } },
|
||||
statistics: { digg_count: 1 },
|
||||
},
|
||||
]);
|
||||
fetchDouyinCommentsMock.mockRejectedValueOnce(new Error('comment API down'));
|
||||
const page = {
|
||||
goto: vi.fn().mockResolvedValue(undefined),
|
||||
wait: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
await expect(command.func(page, {
|
||||
sec_uid: 'MS4w-test',
|
||||
limit: 3,
|
||||
with_comments: true,
|
||||
comment_limit: 5,
|
||||
})).rejects.toBeInstanceOf(CommandExecutionError);
|
||||
});
|
||||
});
|
||||
|
||||
+27
-17
@@ -1,4 +1,5 @@
|
||||
import { cli } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
||||
cli({
|
||||
site: 'jike',
|
||||
name: 'post',
|
||||
@@ -16,16 +17,16 @@ cli({
|
||||
},
|
||||
],
|
||||
columns: ['type', 'author', 'content', 'likes', 'time'],
|
||||
pipeline: [
|
||||
{ navigate: 'https://m.okjike.com/originalPosts/${{ args.id }}' },
|
||||
{ evaluate: `(() => {
|
||||
func: async (page, args) => {
|
||||
await page.goto(`https://m.okjike.com/originalPosts/${args.id}`);
|
||||
const data = await page.evaluate(`(() => {
|
||||
const el = document.querySelector('script[type="application/json"]');
|
||||
if (!el) return { ok: false, reason: 'missing-data-script' };
|
||||
try {
|
||||
const el = document.querySelector('script[type="application/json"]');
|
||||
if (!el) return [];
|
||||
const data = JSON.parse(el.textContent);
|
||||
const data = JSON.parse(el.textContent || '{}');
|
||||
const pageProps = data?.props?.pageProps || {};
|
||||
const post = pageProps.post || {};
|
||||
const comments = pageProps.comments || [];
|
||||
const comments = Array.isArray(pageProps.comments) ? pageProps.comments : [];
|
||||
|
||||
const result = [{
|
||||
type: 'post',
|
||||
@@ -47,16 +48,25 @@ cli({
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
return [];
|
||||
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
||||
}
|
||||
})()
|
||||
` },
|
||||
{ map: {
|
||||
type: '${{ item.type }}',
|
||||
author: '${{ item.author }}',
|
||||
content: '${{ item.content }}',
|
||||
likes: '${{ item.likes }}',
|
||||
time: '${{ item.time }}',
|
||||
} },
|
||||
],
|
||||
`);
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) => ({
|
||||
type: item.type ?? '',
|
||||
author: item.author ?? '',
|
||||
content: item.content ?? '',
|
||||
likes: item.likes ?? 0,
|
||||
time: item.time ?? '',
|
||||
}));
|
||||
}
|
||||
if (data?.reason === 'missing-data-script') {
|
||||
throw new CommandExecutionError('Jike post page did not expose the expected data script');
|
||||
}
|
||||
if (data?.reason === 'parse-error') {
|
||||
throw new CommandExecutionError(`Failed to parse Jike post data: ${data.message || 'unknown error'}`);
|
||||
}
|
||||
throw new CommandExecutionError('Jike post returned an unreadable payload');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { getRegistry } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
import './post.js';
|
||||
import './topic.js';
|
||||
import './user.js';
|
||||
|
||||
function makePage(evaluateResult) {
|
||||
return {
|
||||
goto: vi.fn().mockResolvedValue(undefined),
|
||||
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
||||
};
|
||||
}
|
||||
|
||||
describe('jike read commands', () => {
|
||||
it('maps post rows from the browser-side extractor', async () => {
|
||||
const command = getRegistry().get('jike/post');
|
||||
const page = makePage([
|
||||
{ type: 'post', author: 'alice', content: 'hello', likes: 3, time: '2026-05-16' },
|
||||
{ type: 'comment', author: 'bob', content: 'nice', likes: 1, time: '2026-05-16' },
|
||||
]);
|
||||
|
||||
await expect(command.func(page, { id: 'post-1' })).resolves.toEqual([
|
||||
{ type: 'post', author: 'alice', content: 'hello', likes: 3, time: '2026-05-16' },
|
||||
{ type: 'comment', author: 'bob', content: 'nice', likes: 1, time: '2026-05-16' },
|
||||
]);
|
||||
expect(page.goto).toHaveBeenCalledWith('https://m.okjike.com/originalPosts/post-1');
|
||||
});
|
||||
|
||||
it('maps topic rows and applies limit on the Node side', async () => {
|
||||
const command = getRegistry().get('jike/topic');
|
||||
const page = makePage([
|
||||
{ id: 'a', content: 'one', author: 'alice', likes: 1, comments: 2, time: 't1' },
|
||||
{ id: 'b', content: 'two', author: 'bob', likes: 3, comments: 4, time: 't2' },
|
||||
]);
|
||||
|
||||
await expect(command.func(page, { id: 'topic-1', limit: 1 })).resolves.toEqual([
|
||||
{
|
||||
content: 'one',
|
||||
author: 'alice',
|
||||
likes: 1,
|
||||
comments: 2,
|
||||
time: 't1',
|
||||
url: 'https://web.okjike.com/originalPost/a',
|
||||
},
|
||||
]);
|
||||
expect(page.goto).toHaveBeenCalledWith('https://m.okjike.com/topics/topic-1');
|
||||
});
|
||||
|
||||
it('maps user rows and applies limit on the Node side', async () => {
|
||||
const command = getRegistry().get('jike/user');
|
||||
const page = makePage([
|
||||
{ id: 'a', content: 'one', type: 'post', likes: 1, comments: 2, time: 't1' },
|
||||
{ id: 'b', content: 'two', type: 'repost', likes: 3, comments: 4, time: 't2' },
|
||||
]);
|
||||
|
||||
await expect(command.func(page, { username: 'alice', limit: 1 })).resolves.toEqual([
|
||||
{
|
||||
id: 'a',
|
||||
content: 'one',
|
||||
type: 'post',
|
||||
likes: 1,
|
||||
comments: 2,
|
||||
time: 't1',
|
||||
url: 'https://web.okjike.com/originalPost/a',
|
||||
},
|
||||
]);
|
||||
expect(page.goto).toHaveBeenCalledWith('https://m.okjike.com/users/alice');
|
||||
});
|
||||
|
||||
it('throws CommandExecutionError for malformed browser-side payloads', async () => {
|
||||
await expect(getRegistry().get('jike/post').func(makePage({ reason: 'missing-data-script' }), { id: 'post-1' }))
|
||||
.rejects.toBeInstanceOf(CommandExecutionError);
|
||||
await expect(getRegistry().get('jike/topic').func(makePage({ reason: 'parse-error', message: 'bad json' }), { id: 'topic-1' }))
|
||||
.rejects.toBeInstanceOf(CommandExecutionError);
|
||||
await expect(getRegistry().get('jike/user').func(makePage(null), { username: 'alice' }))
|
||||
.rejects.toBeInstanceOf(CommandExecutionError);
|
||||
});
|
||||
|
||||
it('throws EmptyResultError when topic or user extractors return no posts', async () => {
|
||||
await expect(getRegistry().get('jike/topic').func(makePage([]), { id: 'topic-1' }))
|
||||
.rejects.toBeInstanceOf(EmptyResultError);
|
||||
await expect(getRegistry().get('jike/user').func(makePage([]), { username: 'alice' }))
|
||||
.rejects.toBeInstanceOf(EmptyResultError);
|
||||
});
|
||||
});
|
||||
+32
-19
@@ -1,4 +1,5 @@
|
||||
import { cli } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
cli({
|
||||
site: 'jike',
|
||||
name: 'topic',
|
||||
@@ -17,15 +18,16 @@ cli({
|
||||
{ name: 'limit', type: 'int', default: 20, help: 'Number of posts' },
|
||||
],
|
||||
columns: ['content', 'author', 'likes', 'comments', 'time', 'url'],
|
||||
pipeline: [
|
||||
{ navigate: 'https://m.okjike.com/topics/${{ args.id }}' },
|
||||
{ evaluate: `(() => {
|
||||
func: async (page, args) => {
|
||||
await page.goto(`https://m.okjike.com/topics/${args.id}`);
|
||||
const limit = Number(args.limit) || 20;
|
||||
const data = await page.evaluate(`(() => {
|
||||
const el = document.querySelector('script[type="application/json"]');
|
||||
if (!el) return { ok: false, reason: 'missing-data-script' };
|
||||
try {
|
||||
const el = document.querySelector('script[type="application/json"]');
|
||||
if (!el) return [];
|
||||
const data = JSON.parse(el.textContent);
|
||||
const data = JSON.parse(el.textContent || '{}');
|
||||
const pageProps = data?.props?.pageProps || {};
|
||||
const posts = pageProps.posts || [];
|
||||
const posts = Array.isArray(pageProps.posts) ? pageProps.posts : [];
|
||||
return posts.map(p => ({
|
||||
content: (p.content || '').replace(/\\n/g, ' ').slice(0, 80),
|
||||
author: p.user?.screenName || '',
|
||||
@@ -35,18 +37,29 @@ cli({
|
||||
id: p.id || '',
|
||||
}));
|
||||
} catch (e) {
|
||||
return [];
|
||||
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
||||
}
|
||||
})()
|
||||
` },
|
||||
{ map: {
|
||||
content: '${{ item.content }}',
|
||||
author: '${{ item.author }}',
|
||||
likes: '${{ item.likes }}',
|
||||
comments: '${{ item.comments }}',
|
||||
time: '${{ item.time }}',
|
||||
url: 'https://web.okjike.com/originalPost/${{ item.id }}',
|
||||
} },
|
||||
{ limit: '${{ args.limit }}' },
|
||||
],
|
||||
`);
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) {
|
||||
throw new EmptyResultError('jike topic', `No posts were returned for topic ${args.id}. Confirm the topic ID and login state.`);
|
||||
}
|
||||
return data.slice(0, limit).map((item) => ({
|
||||
content: item.content ?? '',
|
||||
author: item.author ?? '',
|
||||
likes: item.likes ?? 0,
|
||||
comments: item.comments ?? 0,
|
||||
time: item.time ?? '',
|
||||
url: `https://web.okjike.com/originalPost/${item.id ?? ''}`,
|
||||
}));
|
||||
}
|
||||
if (data?.reason === 'missing-data-script') {
|
||||
throw new CommandExecutionError('Jike topic page did not expose the expected data script');
|
||||
}
|
||||
if (data?.reason === 'parse-error') {
|
||||
throw new CommandExecutionError(`Failed to parse Jike topic data: ${data.message || 'unknown error'}`);
|
||||
}
|
||||
throw new CommandExecutionError('Jike topic returned an unreadable payload');
|
||||
},
|
||||
});
|
||||
|
||||
+33
-20
@@ -1,4 +1,5 @@
|
||||
import { cli } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
cli({
|
||||
site: 'jike',
|
||||
name: 'user',
|
||||
@@ -17,14 +18,15 @@ cli({
|
||||
{ name: 'limit', type: 'int', default: 20, help: 'Number of posts' },
|
||||
],
|
||||
columns: ['id', 'content', 'type', 'likes', 'comments', 'time', 'url'],
|
||||
pipeline: [
|
||||
{ navigate: 'https://m.okjike.com/users/${{ args.username }}' },
|
||||
{ evaluate: `(() => {
|
||||
func: async (page, args) => {
|
||||
await page.goto(`https://m.okjike.com/users/${args.username}`);
|
||||
const limit = Number(args.limit) || 20;
|
||||
const data = await page.evaluate(`(() => {
|
||||
const el = document.querySelector('script[type="application/json"]');
|
||||
if (!el) return { ok: false, reason: 'missing-data-script' };
|
||||
try {
|
||||
const el = document.querySelector('script[type="application/json"]');
|
||||
if (!el) return [];
|
||||
const data = JSON.parse(el.textContent);
|
||||
const posts = data?.props?.pageProps?.posts || [];
|
||||
const data = JSON.parse(el.textContent || '{}');
|
||||
const posts = Array.isArray(data?.props?.pageProps?.posts) ? data.props.pageProps.posts : [];
|
||||
return posts.map(p => ({
|
||||
content: (p.content || '').replace(/\\n/g, ' ').slice(0, 80),
|
||||
type: p.type === 'ORIGINAL_POST' ? 'post' : p.type === 'REPOST' ? 'repost' : p.type || '',
|
||||
@@ -34,19 +36,30 @@ cli({
|
||||
id: p.id || '',
|
||||
}));
|
||||
} catch (e) {
|
||||
return [];
|
||||
return { ok: false, reason: 'parse-error', message: e?.message || String(e) };
|
||||
}
|
||||
})()
|
||||
` },
|
||||
{ map: {
|
||||
id: '${{ item.id }}',
|
||||
content: '${{ item.content }}',
|
||||
type: '${{ item.type }}',
|
||||
likes: '${{ item.likes }}',
|
||||
comments: '${{ item.comments }}',
|
||||
time: '${{ item.time }}',
|
||||
url: 'https://web.okjike.com/originalPost/${{ item.id }}',
|
||||
} },
|
||||
{ limit: '${{ args.limit }}' },
|
||||
],
|
||||
`);
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) {
|
||||
throw new EmptyResultError('jike user', `No posts were returned for user ${args.username}. Confirm the username and login state.`);
|
||||
}
|
||||
return data.slice(0, limit).map((item) => ({
|
||||
id: item.id ?? '',
|
||||
content: item.content ?? '',
|
||||
type: item.type ?? '',
|
||||
likes: item.likes ?? 0,
|
||||
comments: item.comments ?? 0,
|
||||
time: item.time ?? '',
|
||||
url: `https://web.okjike.com/originalPost/${item.id ?? ''}`,
|
||||
}));
|
||||
}
|
||||
if (data?.reason === 'missing-data-script') {
|
||||
throw new CommandExecutionError('Jike user page did not expose the expected data script');
|
||||
}
|
||||
if (data?.reason === 'parse-error') {
|
||||
throw new CommandExecutionError(`Failed to parse Jike user data: ${data.message || 'unknown error'}`);
|
||||
}
|
||||
throw new CommandExecutionError('Jike user returned an unreadable payload');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getRegistry } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
import './search.js';
|
||||
describe('weread/search regression', () => {
|
||||
beforeEach(() => {
|
||||
@@ -146,7 +147,7 @@ describe('weread/search regression', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('falls back to empty urls when the search html request fails', async () => {
|
||||
it('surfaces search html request failures instead of emitting empty urls', async () => {
|
||||
const command = getRegistry().get('weread/search');
|
||||
expect(command?.func).toBeTypeOf('function');
|
||||
const fetchMock = vi.fn()
|
||||
@@ -166,16 +167,22 @@ describe('weread/search regression', () => {
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('network timeout'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const result = await command.func({ query: 'deep work', limit: 5 });
|
||||
expect(result).toEqual([
|
||||
{
|
||||
rank: 1,
|
||||
title: 'Deep Work',
|
||||
author: 'Cal Newport',
|
||||
bookId: 'abc123',
|
||||
url: '',
|
||||
},
|
||||
]);
|
||||
await expect(command.func({ query: 'deep work', limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
|
||||
});
|
||||
it('throws EmptyResultError when the public search API returns no books', async () => {
|
||||
const command = getRegistry().get('weread/search');
|
||||
expect(command?.func).toBeTypeOf('function');
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ books: [] }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('<html></html>'),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
await expect(command.func({ query: 'definitely-missing-book', limit: 5 })).rejects.toBeInstanceOf(EmptyResultError);
|
||||
});
|
||||
it('binds reader urls with title and author instead of title alone', async () => {
|
||||
const command = getRegistry().get('weread/search');
|
||||
|
||||
+15
-7
@@ -1,4 +1,5 @@
|
||||
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||||
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
||||
import { fetchWebApi, WEREAD_UA, WEREAD_WEB_ORIGIN } from './utils.js';
|
||||
function decodeHtmlText(value) {
|
||||
return value
|
||||
@@ -84,18 +85,19 @@ function resolveSearchResultUrl(params) {
|
||||
async function loadSearchHtmlEntries(query) {
|
||||
const url = new URL('/web/search/books', WEREAD_WEB_ORIGIN);
|
||||
url.searchParams.set('keyword', query);
|
||||
let html = '';
|
||||
let resp;
|
||||
try {
|
||||
const resp = await fetch(url.toString(), {
|
||||
resp = await fetch(url.toString(), {
|
||||
headers: { 'User-Agent': WEREAD_UA },
|
||||
});
|
||||
if (!resp.ok)
|
||||
return [];
|
||||
html = await resp.text();
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
catch (error) {
|
||||
throw new CommandExecutionError(`Failed to fetch WeRead search page: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw new CommandExecutionError(`WeRead search page request failed: HTTP ${resp.status}`);
|
||||
}
|
||||
const html = await resp.text();
|
||||
const items = Array.from(html.matchAll(/<li[^>]*class="wr_bookList_item"[^>]*>([\s\S]*?)<\/li>/g));
|
||||
return items.map((match) => {
|
||||
const chunk = match[1];
|
||||
@@ -131,6 +133,12 @@ cli({
|
||||
loadSearchHtmlEntries(String(args.query ?? '')),
|
||||
]);
|
||||
const books = data?.books ?? [];
|
||||
if (!Array.isArray(books)) {
|
||||
throw new CommandExecutionError('WeRead search API returned an unreadable books payload');
|
||||
}
|
||||
if (books.length === 0) {
|
||||
throw new EmptyResultError('weread search', `No books were returned for query ${args.query}.`);
|
||||
}
|
||||
const { exactQueues, titleOnlyQueues } = buildSearchUrlQueues(htmlEntries);
|
||||
const apiIdentityCounts = countSearchIdentities(books.map((item) => ({
|
||||
title: item.bookInfo?.title ?? '',
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "douyin/user-videos",
|
||||
"file": "clis/douyin/user-videos.js",
|
||||
"line": 16,
|
||||
"line": 17,
|
||||
"text": "return Math.min(DEFAULT_COMMENT_LIMIT, Math.max(1, Math.round(numeric)));",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -147,7 +147,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "douyin/user-videos",
|
||||
"file": "clis/douyin/user-videos.js",
|
||||
"line": 10,
|
||||
"line": 11,
|
||||
"text": "return Math.min(MAX_USER_VIDEOS_LIMIT, Math.max(1, Math.round(numeric)));",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -411,7 +411,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "linkedin/search",
|
||||
"file": "clis/linkedin/search.js",
|
||||
"line": 249,
|
||||
"line": 256,
|
||||
"text": "const count = Math.min(MAX_BATCH, input.limit - allJobs.length);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -483,7 +483,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "reddit/read",
|
||||
"file": "clis/reddit/read.js",
|
||||
"line": 157,
|
||||
"line": 479,
|
||||
"text": "for (var i = 0; i < Math.min(t1TopLevel.length, limit); i++) {",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -619,7 +619,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/bookmark-folder",
|
||||
"file": "clis/twitter/bookmark-folder.js",
|
||||
"line": 161,
|
||||
"line": 164,
|
||||
"text": "const fetchCount = Math.min(100, limit - allTweets.length + 10);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -627,7 +627,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/bookmarks",
|
||||
"file": "clis/twitter/bookmarks.js",
|
||||
"line": 156,
|
||||
"line": 157,
|
||||
"text": "const fetchCount = Math.min(100, limit - allTweets.length + 10);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -635,7 +635,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/following",
|
||||
"file": "clis/twitter/following.js",
|
||||
"line": 215,
|
||||
"line": 227,
|
||||
"text": "const fetchCount = Math.min(50, limit - allUsers.length + 10);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -643,7 +643,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/likes",
|
||||
"file": "clis/twitter/likes.js",
|
||||
"line": 195,
|
||||
"line": 207,
|
||||
"text": "const fetchCount = Math.min(100, limit - allTweets.length + 10);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -651,7 +651,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/list-tweets",
|
||||
"file": "clis/twitter/list-tweets.js",
|
||||
"line": 168,
|
||||
"line": 174,
|
||||
"text": "const fetchCount = Math.min(100, limit - allTweets.length + 10);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -659,7 +659,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/timeline",
|
||||
"file": "clis/twitter/timeline.js",
|
||||
"line": 182,
|
||||
"line": 181,
|
||||
"text": "const fetchCount = Math.min(40, limit - allTweets.length + 5); // over-fetch slightly for promoted filtering",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -667,7 +667,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/tweets",
|
||||
"file": "clis/twitter/tweets.js",
|
||||
"line": 194,
|
||||
"line": 286,
|
||||
"text": "const fetchCount = Math.min(100, limit - all.length + 10);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -675,7 +675,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "twitter/tweets",
|
||||
"file": "clis/twitter/tweets.js",
|
||||
"line": 159,
|
||||
"line": 231,
|
||||
"text": "const limit = Math.max(1, Math.min(200, kwargs.limit || 20));",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -683,7 +683,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "weibo/comments",
|
||||
"file": "clis/weibo/comments.js",
|
||||
"line": 18,
|
||||
"line": 19,
|
||||
"text": "const count = Math.min(kwargs.limit || 20, 50);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -699,7 +699,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "weibo/hot",
|
||||
"file": "clis/weibo/hot.js",
|
||||
"line": 17,
|
||||
"line": 18,
|
||||
"text": "const count = Math.min(kwargs.limit || 30, 50);",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -707,7 +707,7 @@
|
||||
"rule": "silent-clamp",
|
||||
"command": "weibo/search",
|
||||
"file": "clis/weibo/search.js",
|
||||
"line": 20,
|
||||
"line": 21,
|
||||
"text": "const limit = Math.max(1, Math.min(Number(kwargs.limit) || 10, 50));",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -855,46 +855,6 @@
|
||||
"text": "const limit = Math.max(1, Math.min(Number(args.limit) || 10, 25));",
|
||||
"occurrence": 0
|
||||
},
|
||||
{
|
||||
"rule": "silent-empty-fallback",
|
||||
"command": "douyin/user-videos",
|
||||
"file": "clis/douyin/user-videos.js",
|
||||
"line": 31,
|
||||
"text": "return [];",
|
||||
"occurrence": 0
|
||||
},
|
||||
{
|
||||
"rule": "silent-empty-fallback",
|
||||
"command": "jike/post",
|
||||
"file": "clis/jike/post.js",
|
||||
"line": 50,
|
||||
"text": "return [];",
|
||||
"occurrence": 0
|
||||
},
|
||||
{
|
||||
"rule": "silent-empty-fallback",
|
||||
"command": "jike/topic",
|
||||
"file": "clis/jike/topic.js",
|
||||
"line": 38,
|
||||
"text": "return [];",
|
||||
"occurrence": 0
|
||||
},
|
||||
{
|
||||
"rule": "silent-empty-fallback",
|
||||
"command": "jike/user",
|
||||
"file": "clis/jike/user.js",
|
||||
"line": 37,
|
||||
"text": "return [];",
|
||||
"occurrence": 0
|
||||
},
|
||||
{
|
||||
"rule": "silent-empty-fallback",
|
||||
"command": "weread/search",
|
||||
"file": "clis/weread/search.js",
|
||||
"line": 97,
|
||||
"text": "return [];",
|
||||
"occurrence": 0
|
||||
},
|
||||
{
|
||||
"rule": "silent-sentinel",
|
||||
"command": "36kr/article",
|
||||
@@ -1211,7 +1171,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/article",
|
||||
"file": "clis/twitter/article.js",
|
||||
"line": 105,
|
||||
"line": 109,
|
||||
"text": "const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1219,7 +1179,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/bookmarks",
|
||||
"file": "clis/twitter/bookmarks.js",
|
||||
"line": 53,
|
||||
"line": 55,
|
||||
"text": "const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1227,7 +1187,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/following",
|
||||
"file": "clis/twitter/following.js",
|
||||
"line": 94,
|
||||
"line": 95,
|
||||
"text": "name: core.name || legacy.name || 'unknown',",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1235,7 +1195,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/following",
|
||||
"file": "clis/twitter/following.js",
|
||||
"line": 93,
|
||||
"line": 94,
|
||||
"text": "screen_name: core.screen_name || legacy.screen_name || 'unknown',",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1243,7 +1203,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/likes",
|
||||
"file": "clis/twitter/likes.js",
|
||||
"line": 90,
|
||||
"line": 91,
|
||||
"text": "const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1251,7 +1211,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/list-tweets",
|
||||
"file": "clis/twitter/list-tweets.js",
|
||||
"line": 60,
|
||||
"line": 62,
|
||||
"text": "const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1291,7 +1251,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/search",
|
||||
"file": "clis/twitter/search.js",
|
||||
"line": 296,
|
||||
"line": 217,
|
||||
"text": "author: tweetUser?.core?.screen_name || tweetUser?.legacy?.screen_name || 'unknown',",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1307,7 +1267,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/timeline",
|
||||
"file": "clis/twitter/timeline.js",
|
||||
"line": 75,
|
||||
"line": 76,
|
||||
"text": "const screenName = u?.legacy?.screen_name || u?.core?.screen_name || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1315,7 +1275,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "twitter/tweets",
|
||||
"file": "clis/twitter/tweets.js",
|
||||
"line": 92,
|
||||
"line": 161,
|
||||
"text": "const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1371,7 +1331,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "weibo/comments",
|
||||
"file": "clis/weibo/comments.js",
|
||||
"line": 32,
|
||||
"line": 33,
|
||||
"text": "if (!data.ok) return {error: 'API error: ' + (data.msg || 'unknown')};",
|
||||
"occurrence": 0
|
||||
},
|
||||
@@ -1403,7 +1363,7 @@
|
||||
"rule": "silent-sentinel",
|
||||
"command": "xiaohongshu/download",
|
||||
"file": "clis/xiaohongshu/download.js",
|
||||
"line": 65,
|
||||
"line": 53,
|
||||
"text": "result.author = authorEl?.textContent?.trim() || 'unknown';",
|
||||
"occurrence": 0
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user