@jackwener/opencli 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +6 -7
- package/README.md +21 -362
- package/dist/browser/cdp.js +20 -1
- package/dist/browser/daemon-client.js +3 -2
- package/dist/browser/discover.js +11 -7
- package/dist/browser/dom-helpers.d.ts +11 -0
- package/dist/browser/dom-helpers.js +42 -0
- package/dist/browser/dom-helpers.test.d.ts +1 -0
- package/dist/browser/dom-helpers.test.js +92 -0
- package/dist/browser/index.d.ts +0 -10
- package/dist/browser/index.js +0 -11
- package/dist/browser/mcp.js +4 -3
- package/dist/browser/page.d.ts +2 -0
- package/dist/browser/page.js +42 -3
- package/dist/browser.test.js +17 -8
- package/dist/cli-manifest.json +4 -5
- package/dist/clis/36kr/hot.js +1 -1
- package/dist/clis/36kr/search.js +1 -1
- package/dist/clis/_shared/common.d.ts +8 -0
- package/dist/clis/_shared/common.js +10 -0
- package/dist/clis/apple-podcasts/commands.test.js +26 -3
- package/dist/clis/apple-podcasts/top.js +4 -1
- package/dist/clis/bloomberg/news.js +1 -1
- package/dist/clis/douban/utils.js +3 -6
- package/dist/clis/medium/utils.js +1 -1
- package/dist/clis/producthunt/browse.js +1 -1
- package/dist/clis/producthunt/hot.js +1 -1
- package/dist/clis/sinablog/utils.js +6 -7
- package/dist/clis/substack/utils.js +2 -2
- package/dist/clis/twitter/block.js +1 -1
- package/dist/clis/twitter/bookmark.js +1 -1
- package/dist/clis/twitter/delete.js +1 -1
- package/dist/clis/twitter/follow.js +1 -1
- package/dist/clis/twitter/followers.js +2 -2
- package/dist/clis/twitter/following.js +2 -2
- package/dist/clis/twitter/hide-reply.js +1 -1
- package/dist/clis/twitter/like.js +1 -1
- package/dist/clis/twitter/notifications.js +1 -1
- package/dist/clis/twitter/profile.js +1 -1
- package/dist/clis/twitter/reply-dm.js +1 -1
- package/dist/clis/twitter/reply.js +1 -1
- package/dist/clis/twitter/search.js +1 -1
- package/dist/clis/twitter/unblock.js +1 -1
- package/dist/clis/twitter/unbookmark.js +1 -1
- package/dist/clis/twitter/unfollow.js +1 -1
- package/dist/clis/v2ex/hot.yaml +3 -17
- package/dist/clis/weread/shelf.js +132 -9
- package/dist/clis/weread/utils.js +5 -1
- package/dist/clis/xiaohongshu/comments.test.js +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +1 -0
- package/dist/clis/xiaohongshu/search.test.js +1 -0
- package/dist/daemon.js +1 -0
- package/dist/doctor.js +7 -3
- package/dist/download/index.js +39 -33
- package/dist/download/index.test.js +15 -1
- package/dist/execution.js +3 -8
- package/dist/extension-manifest-regression.test.d.ts +1 -0
- package/dist/extension-manifest-regression.test.js +12 -0
- package/dist/main.js +2 -0
- package/dist/node-network.d.ts +10 -0
- package/dist/node-network.js +174 -0
- package/dist/node-network.test.d.ts +1 -0
- package/dist/node-network.test.js +55 -0
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.test.js +1 -0
- package/dist/pipeline/steps/intercept.js +4 -5
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/dist/weread-private-api-regression.test.js +185 -0
- package/docs/superpowers/plans/2026-03-28-perf-smart-wait.md +1143 -0
- package/docs/superpowers/specs/2026-03-28-perf-smart-wait-design.md +170 -0
- package/extension/dist/background.js +4 -2
- package/extension/manifest.json +4 -1
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +2 -1
- package/package.json +2 -1
- package/src/browser/cdp.ts +21 -0
- package/src/browser/daemon-client.ts +3 -2
- package/src/browser/discover.ts +10 -7
- package/src/browser/dom-helpers.test.ts +100 -0
- package/src/browser/dom-helpers.ts +44 -0
- package/src/browser/index.ts +0 -13
- package/src/browser/mcp.ts +4 -3
- package/src/browser/page.ts +41 -2
- package/src/browser.test.ts +19 -9
- package/src/clis/36kr/hot.ts +1 -1
- package/src/clis/36kr/search.ts +1 -1
- package/src/clis/_shared/common.ts +11 -0
- package/src/clis/apple-podcasts/commands.test.ts +30 -2
- package/src/clis/apple-podcasts/top.ts +4 -1
- package/src/clis/bloomberg/news.ts +1 -1
- package/src/clis/douban/utils.ts +3 -7
- package/src/clis/medium/utils.ts +1 -1
- package/src/clis/producthunt/browse.ts +1 -1
- package/src/clis/producthunt/hot.ts +1 -1
- package/src/clis/sinablog/utils.ts +6 -7
- package/src/clis/substack/utils.ts +2 -2
- package/src/clis/twitter/block.ts +1 -1
- package/src/clis/twitter/bookmark.ts +1 -1
- package/src/clis/twitter/delete.ts +1 -1
- package/src/clis/twitter/follow.ts +1 -1
- package/src/clis/twitter/followers.ts +2 -2
- package/src/clis/twitter/following.ts +2 -2
- package/src/clis/twitter/hide-reply.ts +1 -1
- package/src/clis/twitter/like.ts +1 -1
- package/src/clis/twitter/notifications.ts +1 -1
- package/src/clis/twitter/profile.ts +1 -1
- package/src/clis/twitter/reply-dm.ts +1 -1
- package/src/clis/twitter/reply.ts +1 -1
- package/src/clis/twitter/search.ts +1 -1
- package/src/clis/twitter/unblock.ts +1 -1
- package/src/clis/twitter/unbookmark.ts +1 -1
- package/src/clis/twitter/unfollow.ts +1 -1
- package/src/clis/v2ex/hot.yaml +3 -17
- package/src/clis/weread/shelf.ts +169 -9
- package/src/clis/weread/utils.ts +6 -1
- package/src/clis/xiaohongshu/comments.test.ts +1 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +1 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +1 -0
- package/src/clis/xiaohongshu/publish.test.ts +1 -0
- package/src/clis/xiaohongshu/search.test.ts +1 -0
- package/src/daemon.ts +1 -0
- package/src/doctor.ts +9 -5
- package/src/download/index.test.ts +19 -1
- package/src/download/index.ts +50 -41
- package/src/execution.ts +3 -11
- package/src/extension-manifest-regression.test.ts +17 -0
- package/src/main.ts +3 -0
- package/src/node-network.test.ts +93 -0
- package/src/node-network.ts +213 -0
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.test.ts +1 -0
- package/src/pipeline/steps/intercept.ts +4 -5
- package/src/types.ts +2 -0
- package/src/utils.ts +5 -0
- package/src/weread-private-api-regression.test.ts +207 -0
- package/tests/e2e/browser-public.test.ts +1 -1
- package/tests/e2e/output-formats.test.ts +10 -14
- package/tests/e2e/plugin-management.test.ts +4 -1
- package/tests/e2e/public-commands.test.ts +12 -1
- package/vitest.config.ts +1 -15
package/src/browser.test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BrowserBridge,
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { BrowserBridge, generateStealthJs } from './browser/index.js';
|
|
3
|
+
import { extractTabEntries, diffTabIndexes, appendLimited } from './browser/tabs.js';
|
|
4
|
+
import { withTimeoutMs } from './runtime.js';
|
|
5
|
+
import { __test__ as cdpTest } from './browser/cdp.js';
|
|
6
|
+
import { isRetryableSettleError } from './browser/page.js';
|
|
3
7
|
import * as daemonClient from './browser/daemon-client.js';
|
|
4
8
|
|
|
5
9
|
describe('browser helpers', () => {
|
|
6
10
|
it('extracts tab entries from string snapshots', () => {
|
|
7
|
-
const entries =
|
|
11
|
+
const entries = extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
|
|
8
12
|
|
|
9
13
|
expect(entries).toEqual([
|
|
10
14
|
{ index: 0, identity: 'https://example.com' },
|
|
@@ -13,7 +17,7 @@ describe('browser helpers', () => {
|
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
it('extracts tab entries from MCP markdown format', () => {
|
|
16
|
-
const entries =
|
|
20
|
+
const entries = extractTabEntries(
|
|
17
21
|
'- 0: (current) [Playwright MCP extension](chrome-extension://abc/connect.html)\n- 1: [知乎 - 首页](https://www.zhihu.com/)'
|
|
18
22
|
);
|
|
19
23
|
|
|
@@ -24,7 +28,7 @@ describe('browser helpers', () => {
|
|
|
24
28
|
});
|
|
25
29
|
|
|
26
30
|
it('closes only tabs that were opened during the session', () => {
|
|
27
|
-
const tabsToClose =
|
|
31
|
+
const tabsToClose = diffTabIndexes(
|
|
28
32
|
['https://example.com', 'Chrome Extension'],
|
|
29
33
|
[
|
|
30
34
|
{ index: 0, identity: 'https://example.com' },
|
|
@@ -38,15 +42,21 @@ describe('browser helpers', () => {
|
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
it('keeps only the tail of stderr buffers', () => {
|
|
41
|
-
expect(
|
|
45
|
+
expect(appendLimited('12345', '67890', 8)).toBe('34567890');
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
it('times out slow promises', async () => {
|
|
45
|
-
await expect(
|
|
49
|
+
await expect(withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('retries settle only for target-invalidated errors', () => {
|
|
53
|
+
expect(isRetryableSettleError(new Error('{"code":-32000,"message":"Inspected target navigated or closed"}'))).toBe(true);
|
|
54
|
+
expect(isRetryableSettleError(new Error('attach failed: target no longer exists'))).toBe(false);
|
|
55
|
+
expect(isRetryableSettleError(new Error('malformed exec payload'))).toBe(false);
|
|
46
56
|
});
|
|
47
57
|
|
|
48
58
|
it('prefers the real Electron app target over DevTools and blank pages', () => {
|
|
49
|
-
const target =
|
|
59
|
+
const target = cdpTest.selectCDPTarget([
|
|
50
60
|
{
|
|
51
61
|
type: 'page',
|
|
52
62
|
title: 'DevTools - localhost:9224',
|
|
@@ -73,7 +83,7 @@ describe('browser helpers', () => {
|
|
|
73
83
|
it('honors OPENCLI_CDP_TARGET when multiple inspectable targets exist', () => {
|
|
74
84
|
vi.stubEnv('OPENCLI_CDP_TARGET', 'codex');
|
|
75
85
|
|
|
76
|
-
const target =
|
|
86
|
+
const target = cdpTest.selectCDPTarget([
|
|
77
87
|
{
|
|
78
88
|
type: 'app',
|
|
79
89
|
title: 'Cursor',
|
package/src/clis/36kr/hot.ts
CHANGED
package/src/clis/36kr/search.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for CLI adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Clamp a numeric value to [min, max].
|
|
7
|
+
* Matches the signature of lodash.clamp and Rust's clamp.
|
|
8
|
+
*/
|
|
9
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
10
|
+
return Math.max(min, Math.min(value, max));
|
|
11
|
+
}
|
|
@@ -38,13 +38,14 @@ describe('apple-podcasts search command', () => {
|
|
|
38
38
|
'https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5',
|
|
39
39
|
);
|
|
40
40
|
expect(result).toEqual([
|
|
41
|
-
{
|
|
41
|
+
expect.objectContaining({
|
|
42
42
|
id: 42,
|
|
43
43
|
title: 'Machine Learning Guide',
|
|
44
44
|
author: 'OpenCLI',
|
|
45
45
|
episodes: 12,
|
|
46
46
|
genre: 'Technology',
|
|
47
|
-
|
|
47
|
+
url: '',
|
|
48
|
+
}),
|
|
48
49
|
]);
|
|
49
50
|
});
|
|
50
51
|
});
|
|
@@ -54,6 +55,30 @@ describe('apple-podcasts top command', () => {
|
|
|
54
55
|
vi.restoreAllMocks();
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
it('adds a timeout signal to chart fetches', async () => {
|
|
59
|
+
const cmd = getRegistry().get('apple-podcasts/top');
|
|
60
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
61
|
+
|
|
62
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
63
|
+
ok: true,
|
|
64
|
+
json: () => Promise.resolve({
|
|
65
|
+
feed: {
|
|
66
|
+
results: [
|
|
67
|
+
{ id: '100', name: 'Top Show', artistName: 'Host A' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
73
|
+
|
|
74
|
+
await cmd!.func!(null as any, { country: 'US', limit: 1 });
|
|
75
|
+
|
|
76
|
+
const [, options] = fetchMock.mock.calls[0] ?? [];
|
|
77
|
+
expect(options).toBeDefined();
|
|
78
|
+
expect(options.signal).toBeDefined();
|
|
79
|
+
expect(options.signal).toHaveProperty('aborted', false);
|
|
80
|
+
});
|
|
81
|
+
|
|
57
82
|
it('uses the canonical Apple charts host and maps ranked results', async () => {
|
|
58
83
|
const cmd = getRegistry().get('apple-podcasts/top');
|
|
59
84
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -75,6 +100,9 @@ describe('apple-podcasts top command', () => {
|
|
|
75
100
|
|
|
76
101
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
77
102
|
'https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json',
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
signal: expect.any(Object),
|
|
105
|
+
}),
|
|
78
106
|
);
|
|
79
107
|
expect(result).toEqual([
|
|
80
108
|
{ rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
|
|
@@ -3,6 +3,7 @@ import { CliError } from '../../errors.js';
|
|
|
3
3
|
|
|
4
4
|
// Apple Marketing Tools RSS API — public, no key required
|
|
5
5
|
const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
|
|
6
|
+
const CHARTS_TIMEOUT_MS = 15_000;
|
|
6
7
|
|
|
7
8
|
cli({
|
|
8
9
|
site: 'apple-podcasts',
|
|
@@ -21,7 +22,9 @@ cli({
|
|
|
21
22
|
const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
|
|
22
23
|
let resp: Response;
|
|
23
24
|
try {
|
|
24
|
-
resp = await fetch(url
|
|
25
|
+
resp = await fetch(url, {
|
|
26
|
+
signal: AbortSignal.timeout(CHARTS_TIMEOUT_MS),
|
|
27
|
+
});
|
|
25
28
|
} catch (error: any) {
|
|
26
29
|
const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
|
|
27
30
|
throw new CliError(
|
|
@@ -23,7 +23,7 @@ cli({
|
|
|
23
23
|
|
|
24
24
|
// Navigate and wait for the page to hydrate before extracting story data.
|
|
25
25
|
await page.goto(url);
|
|
26
|
-
await page.wait(5);
|
|
26
|
+
await page.wait({ selector: 'article', timeout: 5 });
|
|
27
27
|
|
|
28
28
|
const loadStory = async () => page.evaluate(`(() => {
|
|
29
29
|
const isRobot = /Are you a robot/i.test(document.title)
|
package/src/clis/douban/utils.ts
CHANGED
|
@@ -4,17 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { ArgumentError, CliError, EmptyResultError } from '../../errors.js';
|
|
6
6
|
import type { IPage } from '../../types.js';
|
|
7
|
+
import { clamp } from '../_shared/common.js';
|
|
7
8
|
|
|
8
9
|
const DOUBAN_PHOTO_PAGE_SIZE = 30;
|
|
9
10
|
const MAX_DOUBAN_PHOTOS = 500;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function clampPhotoLimit(limit: number): number {
|
|
16
|
-
return Math.max(1, Math.min(limit || 120, MAX_DOUBAN_PHOTOS));
|
|
17
|
-
}
|
|
12
|
+
const clampLimit = (limit: number) => clamp(limit || 20, 1, 50);
|
|
13
|
+
const clampPhotoLimit = (limit: number) => clamp(limit || 120, 1, MAX_DOUBAN_PHOTOS);
|
|
18
14
|
|
|
19
15
|
async function ensureDoubanReady(page: IPage): Promise<void> {
|
|
20
16
|
const state = await page.evaluate(`
|
package/src/clis/medium/utils.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function buildMediumUserUrl(username: string): string {
|
|
|
16
16
|
export async function loadMediumPosts(page: IPage, url: string, limit: number): Promise<any[]> {
|
|
17
17
|
if (!page) throw new CommandExecutionError('Browser session required for medium posts');
|
|
18
18
|
await page.goto(url);
|
|
19
|
-
await page.wait(5);
|
|
19
|
+
await page.wait({ selector: 'article', timeout: 5 });
|
|
20
20
|
const data = await page.evaluate(`
|
|
21
21
|
(async () => {
|
|
22
22
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { IPage } from '../../types.js';
|
|
2
|
+
import { clamp } from '../_shared/common.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
return Math.max(1, Math.min(limit || 20, 50));
|
|
5
|
-
}
|
|
4
|
+
const clampLimit = (limit: number) => clamp(limit || 20, 1, 50);
|
|
6
5
|
|
|
7
6
|
export function buildSinaBlogSearchUrl(keyword: string): string {
|
|
8
7
|
return `https://search.sina.com.cn/search?q=${encodeURIComponent(keyword)}&tp=mix`;
|
|
@@ -14,7 +13,7 @@ export function buildSinaBlogUserUrl(uid: string): string {
|
|
|
14
13
|
|
|
15
14
|
export async function loadSinaBlogArticle(page: IPage, url: string): Promise<any> {
|
|
16
15
|
await page.goto(url);
|
|
17
|
-
await page.wait(3);
|
|
16
|
+
await page.wait({ selector: 'h1', timeout: 3 });
|
|
18
17
|
return page.evaluate(`
|
|
19
18
|
(async () => {
|
|
20
19
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
@@ -52,7 +51,7 @@ export async function loadSinaBlogArticle(page: IPage, url: string): Promise<any
|
|
|
52
51
|
export async function loadSinaBlogHot(page: IPage, limit: number): Promise<any[]> {
|
|
53
52
|
const safeLimit = clampLimit(limit);
|
|
54
53
|
await page.goto('https://blog.sina.com.cn/');
|
|
55
|
-
await page.wait(3);
|
|
54
|
+
await page.wait({ selector: 'h1', timeout: 3 });
|
|
56
55
|
const data = await page.evaluate(`
|
|
57
56
|
(async () => {
|
|
58
57
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
@@ -122,7 +121,7 @@ export async function loadSinaBlogHot(page: IPage, limit: number): Promise<any[]
|
|
|
122
121
|
export async function loadSinaBlogSearch(page: IPage, keyword: string, limit: number): Promise<any[]> {
|
|
123
122
|
const safeLimit = clampLimit(limit);
|
|
124
123
|
await page.goto(buildSinaBlogSearchUrl(keyword));
|
|
125
|
-
await page.wait(5);
|
|
124
|
+
await page.wait({ selector: '.result-item', timeout: 5 });
|
|
126
125
|
const data = await page.evaluate(`
|
|
127
126
|
(async () => {
|
|
128
127
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -159,7 +158,7 @@ export async function loadSinaBlogSearch(page: IPage, keyword: string, limit: nu
|
|
|
159
158
|
export async function loadSinaBlogUser(page: IPage, uid: string, limit: number): Promise<any[]> {
|
|
160
159
|
const safeLimit = clampLimit(limit);
|
|
161
160
|
await page.goto(buildSinaBlogUserUrl(uid));
|
|
162
|
-
await page.wait(3);
|
|
161
|
+
await page.wait({ selector: 'h1', timeout: 3 });
|
|
163
162
|
const data = await page.evaluate(`
|
|
164
163
|
(async () => {
|
|
165
164
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -10,7 +10,7 @@ export function buildSubstackBrowseUrl(category?: string): string {
|
|
|
10
10
|
export async function loadSubstackFeed(page: IPage, url: string, limit: number): Promise<any[]> {
|
|
11
11
|
if (!page) throw new CommandExecutionError('Browser session required for substack feed');
|
|
12
12
|
await page.goto(url);
|
|
13
|
-
await page.wait(5);
|
|
13
|
+
await page.wait({ selector: 'article', timeout: 5 });
|
|
14
14
|
const data = await page.evaluate(`
|
|
15
15
|
(async () => {
|
|
16
16
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -79,7 +79,7 @@ export async function loadSubstackFeed(page: IPage, url: string, limit: number):
|
|
|
79
79
|
export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: number): Promise<any[]> {
|
|
80
80
|
if (!page) throw new CommandExecutionError('Browser session required for substack archive');
|
|
81
81
|
await page.goto(`${baseUrl}/archive`);
|
|
82
|
-
await page.wait(5);
|
|
82
|
+
await page.wait({ selector: 'article', timeout: 5 });
|
|
83
83
|
const data = await page.evaluate(`
|
|
84
84
|
(async () => {
|
|
85
85
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
const username = kwargs.username.replace(/^@/, '');
|
|
19
19
|
|
|
20
20
|
await page.goto(`https://x.com/${username}`);
|
|
21
|
-
await page.wait(
|
|
21
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
22
22
|
|
|
23
23
|
const result = await page.evaluate(`(async () => {
|
|
24
24
|
try {
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
if (!page) throw new CommandExecutionError('Browser session required for twitter bookmark');
|
|
18
18
|
|
|
19
19
|
await page.goto(kwargs.url);
|
|
20
|
-
await page.wait(
|
|
20
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
21
21
|
|
|
22
22
|
const result = await page.evaluate(`(async () => {
|
|
23
23
|
try {
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
if (!page) throw new CommandExecutionError('Browser session required for twitter delete');
|
|
18
18
|
|
|
19
19
|
await page.goto(kwargs.url);
|
|
20
|
-
await page.wait(
|
|
20
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' }); // Wait for tweet to load completely
|
|
21
21
|
|
|
22
22
|
const result = await page.evaluate(`(async () => {
|
|
23
23
|
try {
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
const username = kwargs.username.replace(/^@/, '');
|
|
19
19
|
|
|
20
20
|
await page.goto(`https://x.com/${username}`);
|
|
21
|
-
await page.wait(
|
|
21
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
22
22
|
|
|
23
23
|
const result = await page.evaluate(`(async () => {
|
|
24
24
|
try {
|
|
@@ -19,7 +19,7 @@ cli({
|
|
|
19
19
|
// If no user is specified, figure out the logged-in user's handle
|
|
20
20
|
if (!targetUser) {
|
|
21
21
|
await page.goto('https://x.com/home');
|
|
22
|
-
await page.wait(
|
|
22
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
23
23
|
|
|
24
24
|
const href = await page.evaluate(`() => {
|
|
25
25
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
@@ -58,7 +58,7 @@ cli({
|
|
|
58
58
|
if (!clicked) {
|
|
59
59
|
throw new SelectorError('Twitter followers link', 'Twitter may have changed the layout.');
|
|
60
60
|
}
|
|
61
|
-
await page.
|
|
61
|
+
await page.waitForCapture(5);
|
|
62
62
|
|
|
63
63
|
// 4. Scroll to trigger pagination API calls
|
|
64
64
|
await page.autoScroll({ times: Math.ceil(kwargs.limit / 20), delayMs: 2000 });
|
|
@@ -19,7 +19,7 @@ cli({
|
|
|
19
19
|
// If no user is specified, figure out the logged-in user's handle
|
|
20
20
|
if (!targetUser) {
|
|
21
21
|
await page.goto('https://x.com/home');
|
|
22
|
-
await page.wait(
|
|
22
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
23
23
|
|
|
24
24
|
const href = await page.evaluate(`() => {
|
|
25
25
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
@@ -51,7 +51,7 @@ cli({
|
|
|
51
51
|
if (!clicked) {
|
|
52
52
|
throw new SelectorError('Twitter following link', 'Twitter may have changed the layout.');
|
|
53
53
|
}
|
|
54
|
-
await page.
|
|
54
|
+
await page.waitForCapture(5);
|
|
55
55
|
|
|
56
56
|
// 4. Scroll to trigger pagination API calls
|
|
57
57
|
await page.autoScroll({ times: Math.ceil(kwargs.limit / 20), delayMs: 2000 });
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
if (!page) throw new CommandExecutionError('Browser session required for twitter hide-reply');
|
|
18
18
|
|
|
19
19
|
await page.goto(kwargs.url);
|
|
20
|
-
await page.wait(
|
|
20
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
21
21
|
|
|
22
22
|
const result = await page.evaluate(`(async () => {
|
|
23
23
|
try {
|
package/src/clis/twitter/like.ts
CHANGED
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
if (!page) throw new CommandExecutionError('Browser session required for twitter like');
|
|
18
18
|
|
|
19
19
|
await page.goto(kwargs.url);
|
|
20
|
-
await page.wait(
|
|
20
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' }); // Wait for tweet to load completely
|
|
21
21
|
|
|
22
22
|
const result = await page.evaluate(`(async () => {
|
|
23
23
|
try {
|
|
@@ -25,7 +25,7 @@ cli({
|
|
|
25
25
|
window.history.pushState({}, '', '/notifications');
|
|
26
26
|
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
27
27
|
}`);
|
|
28
|
-
await page.
|
|
28
|
+
await page.waitForCapture(5);
|
|
29
29
|
|
|
30
30
|
// Verify SPA navigation succeeded
|
|
31
31
|
const currentUrl = await page.evaluate('() => window.location.pathname');
|
|
@@ -21,7 +21,7 @@ cli({
|
|
|
21
21
|
// If no username, detect the logged-in user
|
|
22
22
|
if (!username) {
|
|
23
23
|
await page.goto('https://x.com/home');
|
|
24
|
-
await page.wait(
|
|
24
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
25
25
|
const href = await page.evaluate(`() => {
|
|
26
26
|
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
27
27
|
return link ? link.getAttribute('href') : null;
|
|
@@ -27,7 +27,7 @@ cli({
|
|
|
27
27
|
|
|
28
28
|
// Step 1: Navigate to messages to get conversation list
|
|
29
29
|
await page.goto('https://x.com/messages');
|
|
30
|
-
await page.wait(
|
|
30
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
31
31
|
|
|
32
32
|
// Step 2: Collect conversations with scroll-to-load
|
|
33
33
|
const needed = maxSend + 10; // extra buffer for skips
|
|
@@ -19,7 +19,7 @@ cli({
|
|
|
19
19
|
|
|
20
20
|
// 1. Navigate to the tweet page
|
|
21
21
|
await page.goto(kwargs.url);
|
|
22
|
-
await page.wait(
|
|
22
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
23
23
|
|
|
24
24
|
// 2. Automate typing the reply and clicking reply
|
|
25
25
|
const result = await page.evaluate(`(async () => {
|
|
@@ -20,7 +20,7 @@ async function navigateToSearch(page: Pick<IPage, 'evaluate' | 'wait'>, query: s
|
|
|
20
20
|
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
21
21
|
})()
|
|
22
22
|
`);
|
|
23
|
-
await page.wait(
|
|
23
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
24
24
|
|
|
25
25
|
lastPath = String(await page.evaluate('() => window.location.pathname') || '');
|
|
26
26
|
if (lastPath.startsWith('/search')) {
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
const username = kwargs.username.replace(/^@/, '');
|
|
19
19
|
|
|
20
20
|
await page.goto(`https://x.com/${username}`);
|
|
21
|
-
await page.wait(
|
|
21
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
22
22
|
|
|
23
23
|
const result = await page.evaluate(`(async () => {
|
|
24
24
|
try {
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
if (!page) throw new CommandExecutionError('Browser session required for twitter unbookmark');
|
|
18
18
|
|
|
19
19
|
await page.goto(kwargs.url);
|
|
20
|
-
await page.wait(
|
|
20
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
21
21
|
|
|
22
22
|
const result = await page.evaluate(`(async () => {
|
|
23
23
|
try {
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
const username = kwargs.username.replace(/^@/, '');
|
|
19
19
|
|
|
20
20
|
await page.goto(`https://x.com/${username}`);
|
|
21
|
-
await page.wait(
|
|
21
|
+
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
22
22
|
|
|
23
23
|
const result = await page.evaluate(`(async () => {
|
|
24
24
|
try {
|
package/src/clis/v2ex/hot.yaml
CHANGED
|
@@ -3,7 +3,7 @@ name: hot
|
|
|
3
3
|
description: V2EX 热门话题
|
|
4
4
|
domain: www.v2ex.com
|
|
5
5
|
strategy: public
|
|
6
|
-
browser:
|
|
6
|
+
browser: false
|
|
7
7
|
|
|
8
8
|
args:
|
|
9
9
|
limit:
|
|
@@ -12,22 +12,8 @@ args:
|
|
|
12
12
|
description: Number of topics
|
|
13
13
|
|
|
14
14
|
pipeline:
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
- evaluate: |
|
|
18
|
-
(async () => {
|
|
19
|
-
const response = await fetch('/api/topics/hot.json', {
|
|
20
|
-
credentials: 'include',
|
|
21
|
-
headers: {
|
|
22
|
-
accept: 'application/json, text/plain, */*',
|
|
23
|
-
'x-requested-with': 'XMLHttpRequest',
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
if (!response.ok) {
|
|
27
|
-
throw new Error(`V2EX hot API request failed: ${response.status}`);
|
|
28
|
-
}
|
|
29
|
-
return await response.json();
|
|
30
|
-
})()
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://www.v2ex.com/api/topics/hot.json
|
|
31
17
|
|
|
32
18
|
- map:
|
|
33
19
|
rank: ${{ index + 1 }}
|