Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8d6319f87 | |||
| bf6207a4e2 | |||
| 4e669629fd | |||
| 6cf14e0800 | |||
| 45370b50e1 |
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}/`);
|
||||
|
||||
@@ -24,7 +25,7 @@ cli({
|
||||
})()`);
|
||||
|
||||
if (!cid) {
|
||||
throw new Error('无法在页面中提取到当前视频的 CID,请检查页面是否正常加载。');
|
||||
throw new SelectorError('videoData.cid', '无法在页面中提取到当前视频的 CID,请检查页面是否正常加载。');
|
||||
}
|
||||
|
||||
// 3. 在 Node 端使用 apiGet 获取带 Wbi 签名的字幕列表
|
||||
@@ -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. 选择目标字幕语言
|
||||
@@ -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;
|
||||
@@ -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. 数据映射
|
||||
|
||||
@@ -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,
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
requirePage, navigateToChat, findFriendByUid,
|
||||
clickCandidateInList, typeAndSendMessage,
|
||||
} from './common.js';
|
||||
import { EmptyResultError, SelectorError } from '../../errors.js';
|
||||
|
||||
cli({
|
||||
site: 'boss',
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────
|
||||
|
||||
@@ -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)];
|
||||
@@ -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];
|
||||
@@ -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 : [];
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CommandExecutionError } from '../../errors.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
export function buildMediumTagUrl(topic?: string): string {
|
||||
@@ -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(`
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* - Indented output showing conversation threads
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CommandExecutionError } from '../../errors.js';
|
||||
|
||||
cli({
|
||||
site: 'reddit',
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CommandExecutionError } from '../../errors.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
||||
|
||||
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
||||
|
||||
@@ -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[] = [];
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CommandExecutionError } from '../../errors.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
cli({
|
||||
@@ -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}`);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type RawSegment,
|
||||
type Chapter,
|
||||
} from './transcript-group.js';
|
||||
import { CommandExecutionError, EmptyResultError } from '../../errors.js';
|
||||
|
||||
cli({
|
||||
site: 'youtube',
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { parseVideoId } from './utils.js';
|
||||
import { CommandExecutionError } from '../../errors.js';
|
||||
|
||||
cli({
|
||||
site: 'youtube',
|
||||
@@ -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]) => ({
|
||||
|
||||
Reference in New Issue
Block a user