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:
jakevin
2026-05-16 16:43:13 +08:00
committed by GitHub
parent 854cf01aad
commit 716461581a
10 changed files with 295 additions and 142 deletions
+6
View File
@@ -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.
+9 -2
View File
@@ -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,
+43
View File
@@ -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
View File
@@ -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');
},
});
+86
View File
@@ -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
View File
@@ -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
View File
@@ -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');
},
});
+18 -11
View File
@@ -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
View File
@@ -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 ?? '',
+26 -66
View File
@@ -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
},