Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/clis/bilibili/following.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { CommandExecutionError } from '../../errors.js';
import type { IPage } from '../../types.js';
import { fetchJson, getSelfUid, resolveUid } from './utils.js';

Expand All @@ -14,7 +15,7 @@ cli({
],
columns: ['mid', 'name', 'sign', 'following', 'fans'],
func: async (page: IPage | null, kwargs: any) => {
if (!page) throw new Error('Requires browser');
if (!page) throw new CommandExecutionError('Browser session required for bilibili following');

// 1. Resolve UID (default to self)
const uid = kwargs.uid
Expand All @@ -30,7 +31,7 @@ cli({
);

if (payload.code !== 0) {
throw new Error(`获取关注列表失败: ${payload.message} (${payload.code})`);
throw new CommandExecutionError(`获取关注列表失败: ${payload.message} (${payload.code})`);
}

const list = payload.data?.list || [];
Expand Down
15 changes: 8 additions & 7 deletions src/clis/bilibili/subtitle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { AuthRequiredError, CommandExecutionError, EmptyResultError, SelectorError } from '../../errors.js';
import type { IPage } from '../../types.js';
import { apiGet } from './utils.js';

Expand All @@ -13,7 +14,7 @@ cli({
],
columns: ['index', 'from', 'to', 'content'],
func: async (page: IPage | null, kwargs: any) => {
if (!page) throw new Error('Requires browser');
if (!page) throw new CommandExecutionError('Browser session required for bilibili subtitle');
// 1. 先前往视频详情页 (建立有鉴权的 Session,且这里不需要加载完整个视频)
await page.goto(`https://www.bilibili.com/video/${kwargs.bvid}/`);

Expand All @@ -24,7 +25,7 @@ cli({
})()`);

if (!cid) {
throw new Error('无法在页面中提取到当前视频的 CID,请检查页面是否正常加载。');
throw new SelectorError('videoData.cid', '无法在页面中提取到当前视频的 CID,请检查页面是否正常加载。');
}

// 3. 在 Node 端使用 apiGet 获取带 Wbi 签名的字幕列表
Expand All @@ -35,12 +36,12 @@ cli({
});

if (payload.code !== 0) {
throw new Error(`获取视频播放信息失败: ${payload.message} (${payload.code})`);
throw new CommandExecutionError(`获取视频播放信息失败: ${payload.message} (${payload.code})`);
}

const subtitles = payload.data?.subtitle?.subtitles || [];
if (subtitles.length === 0) {
throw new Error('此视频没有发现外挂或智能字幕。');
throw new EmptyResultError('bilibili subtitle', '此视频没有发现外挂或智能字幕。');
}

// 4. 选择目标字幕语言
Expand All @@ -50,7 +51,7 @@ cli({

const targetSubUrl = target.subtitle_url;
if (!targetSubUrl || targetSubUrl === '') {
throw new Error('[风控拦截/未登录] 获取到的 subtitle_url 为空!请确保 CLI 已成功登录且风控未封锁此账号。');
throw new AuthRequiredError('bilibili.com', '[风控拦截/未登录] 获取到的 subtitle_url 为空!请确保 CLI 已成功登录且风控未封锁此账号。');
}

const finalUrl = targetSubUrl.startsWith('//') ? 'https:' + targetSubUrl : targetSubUrl;
Expand Down Expand Up @@ -81,12 +82,12 @@ cli({
const items = await page.evaluate(fetchJs);

if (items?.error) {
throw new Error(`字幕获取失败: ${items.error}${items.text ? ' — ' + items.text : ''}`);
throw new CommandExecutionError(`字幕获取失败: ${items.error}${items.text ? ' — ' + items.text : ''}`);
}

const finalItems = items?.data || [];
if (!Array.isArray(finalItems)) {
throw new Error('解析到的字幕列表对象不符合数组格式');
throw new CommandExecutionError('解析到的字幕列表对象不符合数组格式');
}

// 6. 数据映射
Expand Down
4 changes: 2 additions & 2 deletions src/clis/bilibili/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import type { IPage } from '../../types.js';
import { AuthRequiredError } from '../../errors.js';
import { AuthRequiredError, EmptyResultError } from '../../errors.js';

const MIXIN_KEY_ENC_TAB = [
46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,
Expand Down Expand Up @@ -112,5 +112,5 @@ export async function resolveUid(page: IPage, input: string): Promise<string> {
});
const results = payload?.data?.result ?? [];
if (results.length > 0) return String(results[0].mid);
throw new Error(`Cannot resolve UID for: ${input}`);
throw new EmptyResultError(`bilibili user search: ${input}`, 'User may not exist or username may have changed.');
}
5 changes: 3 additions & 2 deletions src/clis/boss/mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { cli, Strategy } from '../../registry.js';
import { requirePage, navigateToChat, bossFetch, findFriendByUid, verbose } from './common.js';
import { ArgumentError, EmptyResultError } from '../../errors.js';

const LABEL_MAP: Record<string, number> = {
'新招呼': 1, '沟通中': 2, '已约面': 3, '已获取简历': 4,
Expand Down Expand Up @@ -44,7 +45,7 @@ cli({
if (entry) {
labelId = entry[1];
} else {
throw new Error(`未知标签: ${labelInput}。可用标签: ${Object.keys(LABEL_MAP).join(', ')}`);
throw new ArgumentError(`未知标签: ${labelInput}。可用标签: ${Object.keys(LABEL_MAP).join(', ')}`);
}
}

Expand All @@ -53,7 +54,7 @@ cli({
await navigateToChat(page);

const friend = await findFriendByUid(page, kwargs.uid, { checkGreetList: true });
if (!friend) throw new Error('未找到该候选人');
if (!friend) throw new EmptyResultError('boss candidate search');

const friendName = friend.name || '候选人';
const action = remove ? 'deleteMark' : 'addMark';
Expand Down
7 changes: 4 additions & 3 deletions src/clis/boss/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
requirePage, navigateToChat, findFriendByUid,
clickCandidateInList, typeAndSendMessage,
} from './common.js';
import { EmptyResultError, SelectorError } from '../../errors.js';

cli({
site: 'boss',
Expand All @@ -29,21 +30,21 @@ cli({
await navigateToChat(page, 3);

const friend = await findFriendByUid(page, kwargs.uid, { maxPages: 5 });
if (!friend) throw new Error('未找到该候选人,请确认 uid 是否正确');
if (!friend) throw new EmptyResultError('boss candidate search', '请确认 uid 是否正确');

const numericUid = friend.uid;
const friendName = friend.name || '候选人';

const clicked = await clickCandidateInList(page, numericUid);
if (!clicked) {
throw new Error('无法在聊天列表中找到该用户,请确认聊天列表中有此人');
throw new SelectorError('聊天列表中的用户', '请确认聊天列表中有此人');
}

await page.wait({ time: 2 });

const sent = await typeAndSendMessage(page, kwargs.text);
if (!sent) {
throw new Error('找不到消息输入框');
throw new SelectorError('消息输入框', '聊天页面 UI 可能已改变');
}

await page.wait({ time: 1 });
Expand Down
9 changes: 5 additions & 4 deletions src/clis/linkedin/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
import { ArgumentError, CommandExecutionError } from '../../errors.js';

// ── Filter value mappings ──────────────────────────────────────────────

Expand Down Expand Up @@ -64,7 +65,7 @@ function mapFilterValues(input: unknown, mapping: Record<string, string>, label:
const resolved = values.map(value => {
const key = value.toLowerCase();
const mapped = mapping[key];
if (!mapped) throw new Error(`Unsupported ${label}: ${value}`);
if (!mapped) throw new ArgumentError(`Unsupported ${label}: ${value}`);
return mapped;
});
return [...new Set(resolved)];
Expand Down Expand Up @@ -214,7 +215,7 @@ async function resolveCompanyIds(page: IPage, input: unknown): Promise<string[]>
}

if (unresolved.length) {
throw new Error(`Could not resolve LinkedIn company filter: ${unresolved.join(', ')}`);
throw new ArgumentError(`Could not resolve LinkedIn company filter: ${unresolved.join(', ')}`);
}

return [...ids];
Expand Down Expand Up @@ -252,7 +253,7 @@ async function fetchJobCards(
})()`);

if (!batch || batch.error) {
throw new Error(batch?.error || 'LinkedIn search returned an unexpected response');
throw new CommandExecutionError(batch?.error || 'LinkedIn search returned an unexpected response');
}

const elements: any[] = Array.isArray(batch?.elements) ? batch.elements : [];
Expand Down Expand Up @@ -387,7 +388,7 @@ cli({
const location = (kwargs.location ?? '').trim();
const keywords = String(kwargs.query ?? '').trim();

if (!keywords) throw new Error('query is required');
if (!keywords) throw new ArgumentError('query is required');

const searchParams = new URLSearchParams({ keywords });
if (location) searchParams.set('location', location);
Expand Down
5 changes: 3 additions & 2 deletions src/clis/linkedin/timeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
import { AuthRequiredError, EmptyResultError } from '../../errors.js';

interface TimelinePost {
rank?: number;
Expand Down Expand Up @@ -510,11 +511,11 @@ cli({
}

if (sawLoginWall && posts.length === 0) {
throw new Error('LinkedIn timeline requires an active signed-in browser session');
throw new AuthRequiredError('linkedin.com', 'LinkedIn timeline requires an active signed-in browser session');
}

if (posts.length === 0) {
throw new Error('No LinkedIn timeline posts found. Make sure your LinkedIn home feed is visible in the browser.');
throw new EmptyResultError('linkedin timeline', 'Make sure your LinkedIn home feed is visible in the browser.');
}

return posts.slice(0, limit).map((post, index) => ({
Expand Down
3 changes: 2 additions & 1 deletion src/clis/medium/shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CommandExecutionError } from '../../errors.js';
import type { IPage } from '../../types.js';

export function buildMediumTagUrl(topic?: string): string {
Expand All @@ -13,7 +14,7 @@ export function buildMediumUserUrl(username: string): string {
}

export async function loadMediumPosts(page: IPage, url: string, limit: number): Promise<any[]> {
if (!page) throw new Error('Requires browser session');
if (!page) throw new CommandExecutionError('Browser session required for medium posts');
await page.goto(url);
await page.wait(5);
const data = await page.evaluate(`
Expand Down
7 changes: 4 additions & 3 deletions src/clis/reddit/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* - Indented output showing conversation threads
*/
import { cli, Strategy } from '../../registry.js';
import { CommandExecutionError } from '../../errors.js';

cli({
site: 'reddit',
Expand Down Expand Up @@ -176,9 +177,9 @@ cli({
})()
`);

if (!data || typeof data !== 'object') throw new Error('Failed to fetch post data');
if (!Array.isArray(data) && data.error) throw new Error(data.error);
if (!Array.isArray(data)) throw new Error('Unexpected response');
if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to fetch post data');
if (!Array.isArray(data) && data.error) throw new CommandExecutionError(data.error);
if (!Array.isArray(data)) throw new CommandExecutionError('Unexpected response');

return data;
},
Expand Down
5 changes: 3 additions & 2 deletions src/clis/twitter/bookmarks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { AuthRequiredError, CommandExecutionError } from '../../errors.js';

const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
const BOOKMARKS_QUERY_ID = 'Fy0QMy4q_aZCpkO0PnyLYw';
Expand Down Expand Up @@ -137,7 +138,7 @@ cli({
const ct0 = await page.evaluate(`() => {
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
}`);
if (!ct0) throw new Error('Not logged into x.com (no ct0 cookie)');
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');

const queryId = await page.evaluate(`async () => {
try {
Expand Down Expand Up @@ -185,7 +186,7 @@ cli({
}`);

if (data?.error) {
if (allTweets.length === 0) throw new Error(`HTTP ${data.error}: Failed to fetch bookmarks. queryId may have expired.`);
if (allTweets.length === 0) throw new CommandExecutionError(`HTTP ${data.error}: Failed to fetch bookmarks. queryId may have expired.`);
break;
}

Expand Down
3 changes: 2 additions & 1 deletion src/clis/twitter/delete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { CommandExecutionError } from '../../errors.js';
import type { IPage } from '../../types.js';

cli({
Expand All @@ -13,7 +14,7 @@ cli({
],
columns: ['status', 'message'],
func: async (page: IPage | null, kwargs: any) => {
if (!page) throw new Error('Requires browser');
if (!page) throw new CommandExecutionError('Browser session required for twitter delete');

await page.goto(kwargs.url);
await page.wait(5); // Wait for tweet to load completely
Expand Down
5 changes: 3 additions & 2 deletions src/clis/twitter/trending.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { AuthRequiredError, EmptyResultError } from '../../errors.js';

// ── Twitter GraphQL constants ──────────────────────────────────────────

Expand Down Expand Up @@ -37,7 +38,7 @@ cli({
const ct0 = await page.evaluate(`(() => {
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
})()`);
if (!ct0) throw new Error('Not logged into x.com (no ct0 cookie)');
if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');

// Try legacy guide.json API first (faster than DOM scraping)
let trends: TrendItem[] = [];
Expand Down Expand Up @@ -105,7 +106,7 @@ cli({
}

if (trends.length === 0) {
throw new Error('No trending data found. API may have changed or login may be required.');
throw new EmptyResultError('twitter trending', 'API may have changed or login may be required.');
}

return trends.slice(0, limit);
Expand Down
3 changes: 2 additions & 1 deletion src/clis/twitter/unfollow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import { CommandExecutionError } from '../../errors.js';
import type { IPage } from '../../types.js';

cli({
Expand All @@ -13,7 +14,7 @@ cli({
],
columns: ['status', 'message'],
func: async (page: IPage | null, kwargs: any) => {
if (!page) throw new Error('Requires browser');
if (!page) throw new CommandExecutionError('Browser session required for twitter unfollow');
const username = kwargs.username.replace(/^@/, '');

await page.goto(`https://x.com/${username}`);
Expand Down
9 changes: 5 additions & 4 deletions src/clis/youtube/transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type RawSegment,
type Chapter,
} from './transcript-group.js';
import { CommandExecutionError, EmptyResultError } from '../../errors.js';

cli({
site: 'youtube',
Expand Down Expand Up @@ -91,10 +92,10 @@ cli({
`);

if (!captionData || typeof captionData === 'string') {
throw new Error(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
throw new CommandExecutionError(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
}
if (captionData.error) {
throw new Error(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
throw new CommandExecutionError(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
}

// Warn if --lang was specified but not matched
Expand Down Expand Up @@ -176,10 +177,10 @@ cli({
`);

if (!Array.isArray(segments)) {
throw new Error((segments as any)?.error || 'Failed to parse caption segments');
throw new CommandExecutionError((segments as any)?.error || 'Failed to parse caption segments');
}
if (segments.length === 0) {
throw new Error('No caption segments found');
throw new EmptyResultError('youtube transcript');
}

// Step 3: Fetch chapters (for grouped mode)
Expand Down
5 changes: 3 additions & 2 deletions src/clis/youtube/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { cli, Strategy } from '../../registry.js';
import { parseVideoId } from './utils.js';
import { CommandExecutionError } from '../../errors.js';

cli({
site: 'youtube',
Expand Down Expand Up @@ -104,8 +105,8 @@ cli({
})()
`);

if (!data || typeof data !== 'object') throw new Error('Failed to extract video metadata from page');
if (data.error) throw new Error(data.error);
if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to extract video metadata from page');
if (data.error) throw new CommandExecutionError(data.error);

// Return as field/value pairs for table display
return Object.entries(data).map(([field, value]) => ({
Expand Down
Loading