@jackwener/opencli 1.7.8 → 1.7.10
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/README.md +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +646 -30
- package/clis/36kr/news.js +1 -1
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +1 -1
- package/clis/chatgpt-app/ax.js +4 -2
- package/clis/chatgpt-app/ax.test.js +12 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +1 -1
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +17 -4
- package/clis/deepseek/ask.test.js +46 -0
- package/clis/deepseek/utils.js +55 -16
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/instagram/collection-create.js +57 -0
- package/clis/instagram/saved.js +21 -7
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +1 -1
- package/clis/sinafinance/stock.test.js +2 -2
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +5 -6
- package/clis/toutiao/articles.test.js +22 -15
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -2
- package/dist/src/browser/bridge.js +40 -41
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
- package/dist/src/browser/daemon-lifecycle.js +67 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +477 -35
- package/dist/src/cli.test.js +303 -2
- package/dist/src/commanderAdapter.js +17 -9
- package/dist/src/commanderAdapter.test.js +67 -2
- package/dist/src/commands/daemon.d.ts +2 -0
- package/dist/src/commands/daemon.js +42 -1
- package/dist/src/commands/daemon.test.js +103 -2
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +5 -6
- package/dist/src/doctor.js +77 -19
- package/dist/src/doctor.test.js +117 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
package/clis/lesswrong/user.js
CHANGED
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
columns: ['field', 'value'],
|
|
21
|
-
func: async (
|
|
21
|
+
func: async (kwargs) => {
|
|
22
22
|
const slug = gqlEscape(String(kwargs.username).toLowerCase());
|
|
23
23
|
const query = `query UserProfile {
|
|
24
24
|
user(input: {selector: {slug: "${slug}"}}) {
|
|
@@ -38,7 +38,7 @@ describe('paperreview submit command', () => {
|
|
|
38
38
|
resolvedPath: '/tmp/paper.pdf',
|
|
39
39
|
sizeBytes: 4096,
|
|
40
40
|
});
|
|
41
|
-
const result = await cmd.func(
|
|
41
|
+
const result = await cmd.func({
|
|
42
42
|
pdf: './paper.pdf',
|
|
43
43
|
email: 'wang2629651228@gmail.com',
|
|
44
44
|
venue: 'RAL',
|
|
@@ -80,7 +80,7 @@ describe('paperreview submit command', () => {
|
|
|
80
80
|
message: 'Submission accepted',
|
|
81
81
|
},
|
|
82
82
|
});
|
|
83
|
-
const result = await cmd.func(
|
|
83
|
+
const result = await cmd.func({
|
|
84
84
|
pdf: './paper.pdf',
|
|
85
85
|
email: 'wang2629651228@gmail.com',
|
|
86
86
|
venue: 'RAL',
|
|
@@ -112,7 +112,7 @@ describe('paperreview submit command', () => {
|
|
|
112
112
|
s3_key: 'uploads/paper.pdf',
|
|
113
113
|
},
|
|
114
114
|
});
|
|
115
|
-
const result = await cmd.func(
|
|
115
|
+
const result = await cmd.func({
|
|
116
116
|
pdf: './paper.pdf',
|
|
117
117
|
email: 'wang2629651228@gmail.com',
|
|
118
118
|
venue: 'RAL',
|
|
@@ -153,7 +153,7 @@ describe('paperreview submit command', () => {
|
|
|
153
153
|
message: 'Submission accepted',
|
|
154
154
|
},
|
|
155
155
|
});
|
|
156
|
-
const result = await cmd.func(
|
|
156
|
+
const result = await cmd.func({
|
|
157
157
|
pdf: './paper.pdf',
|
|
158
158
|
email: 'wang2629651228@gmail.com',
|
|
159
159
|
venue: 'RAL',
|
|
@@ -190,7 +190,7 @@ describe('paperreview review command', () => {
|
|
|
190
190
|
response: { status: 202 },
|
|
191
191
|
payload: { detail: 'Review is still processing.' },
|
|
192
192
|
});
|
|
193
|
-
const result = await cmd.func(
|
|
193
|
+
const result = await cmd.func({ token: 'tok_123' });
|
|
194
194
|
expect(result).toMatchObject({
|
|
195
195
|
status: 'processing',
|
|
196
196
|
token: 'tok_123',
|
|
@@ -214,7 +214,7 @@ describe('paperreview feedback command', () => {
|
|
|
214
214
|
response: { ok: true, status: 200 },
|
|
215
215
|
payload: { message: 'Thanks for the feedback.' },
|
|
216
216
|
});
|
|
217
|
-
const result = await cmd.func(
|
|
217
|
+
const result = await cmd.func({
|
|
218
218
|
token: 'tok_123',
|
|
219
219
|
helpfulness: 4,
|
|
220
220
|
'critical-error': 'yes',
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
{ name: 'additional-comments', help: 'Optional free-text feedback' },
|
|
18
18
|
],
|
|
19
19
|
columns: ['status', 'token', 'helpfulness', 'critical_error', 'actionable_suggestions', 'message'],
|
|
20
|
-
func: async (
|
|
20
|
+
func: async (kwargs) => {
|
|
21
21
|
const token = String(kwargs.token ?? '').trim();
|
|
22
22
|
if (!token) {
|
|
23
23
|
throw new CliError('ARGUMENT', 'A review token is required.');
|
|
@@ -13,7 +13,7 @@ cli({
|
|
|
13
13
|
{ name: 'token', positional: true, required: true, help: 'Review token returned by paperreview.ai' },
|
|
14
14
|
],
|
|
15
15
|
columns: ['status', 'title', 'venue', 'numerical_score', 'has_feedback', 'review_url'],
|
|
16
|
-
func: async (
|
|
16
|
+
func: async (kwargs) => {
|
|
17
17
|
const token = String(kwargs.token ?? '').trim();
|
|
18
18
|
if (!token) {
|
|
19
19
|
throw new CliError('ARGUMENT', 'A review token is required.');
|
|
@@ -24,7 +24,7 @@ cli({
|
|
|
24
24
|
return 'prepared only';
|
|
25
25
|
return undefined;
|
|
26
26
|
},
|
|
27
|
-
func: async (
|
|
27
|
+
func: async (kwargs) => {
|
|
28
28
|
const pdfFile = await readPdfFile(kwargs.pdf);
|
|
29
29
|
const email = String(kwargs.email ?? '').trim();
|
|
30
30
|
const venue = normalizeVenue(kwargs.venue);
|
|
@@ -19,7 +19,7 @@ cli({
|
|
|
19
19
|
},
|
|
20
20
|
],
|
|
21
21
|
columns: ['rank', 'name', 'tagline', 'author', 'date', 'url'],
|
|
22
|
-
func: async (
|
|
22
|
+
func: async (args) => {
|
|
23
23
|
const count = Math.min(Number(args.limit) || 20, 50);
|
|
24
24
|
const category = String(args.category ?? '').trim() || undefined;
|
|
25
25
|
const posts = await fetchFeed(category);
|
|
@@ -16,7 +16,7 @@ cli({
|
|
|
16
16
|
{ name: 'limit', type: 'int', default: 20, help: 'Max results' },
|
|
17
17
|
],
|
|
18
18
|
columns: ['rank', 'name', 'tagline', 'author', 'url'],
|
|
19
|
-
func: async (
|
|
19
|
+
func: async (args) => {
|
|
20
20
|
const count = Math.min(Number(args.limit) || 20, 50);
|
|
21
21
|
const posts = await fetchFeed();
|
|
22
22
|
if (posts.length === 0)
|
package/clis/sinablog/search.js
CHANGED
|
@@ -47,5 +47,5 @@ cli({
|
|
|
47
47
|
{ name: 'limit', type: 'int', default: 20, help: '返回的文章数量' },
|
|
48
48
|
],
|
|
49
49
|
columns: ['rank', 'title', 'author', 'date', 'description', 'url'],
|
|
50
|
-
func: async (
|
|
50
|
+
func: async (args) => searchSinaBlog(args.keyword, Math.max(1, Math.min(Number(args.limit) || 20, 50))),
|
|
51
51
|
});
|
package/clis/sinafinance/news.js
CHANGED
|
@@ -34,7 +34,7 @@ cli({
|
|
|
34
34
|
{ name: 'type', type: 'int', default: 0, help: 'News type: 0=全部 1=A股 2=宏观 3=公司 4=数据 5=市场 6=国际 7=观点 8=央行 9=其它' },
|
|
35
35
|
],
|
|
36
36
|
columns: ['id', 'time', 'content', 'views'],
|
|
37
|
-
func: async (
|
|
37
|
+
func: async (args) => {
|
|
38
38
|
const limit = Math.max(1, Math.min(Number(args.limit), 50));
|
|
39
39
|
const apiTag = TYPE_MAP[args.type] ?? 0;
|
|
40
40
|
const params = new URLSearchParams({
|
|
@@ -62,7 +62,7 @@ cli({
|
|
|
62
62
|
{ name: 'market', type: 'string', default: 'auto', help: 'Market: cn, hk, us, auto (default: auto searches cn → hk → us)' },
|
|
63
63
|
],
|
|
64
64
|
columns: ['Symbol', 'Name', 'Price', 'Change', 'ChangePercent', 'Open', 'High', 'Low', 'Volume', 'MarketCap'],
|
|
65
|
-
func: async (
|
|
65
|
+
func: async (args) => {
|
|
66
66
|
const key = String(args.key);
|
|
67
67
|
const market = String(args.market);
|
|
68
68
|
const marketMap = {
|
|
@@ -28,7 +28,7 @@ describe('sinafinance stock command', () => {
|
|
|
28
28
|
.mockResolvedValueOnce(textResponse('var hq_str_gb_AAPL="Apple Inc,189.98,1.23,0,1.56,0,188.50,180.00,195.00,175.00,1200000,0,3000000000000";'));
|
|
29
29
|
vi.stubGlobal('fetch', fetchMock);
|
|
30
30
|
|
|
31
|
-
const result = await cmd.func(
|
|
31
|
+
const result = await cmd.func({ key: 'AAPL', market: 'auto' });
|
|
32
32
|
|
|
33
33
|
expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://suggest3.sinajs.cn/suggest/type=11,31,41&key=AAPL', expect.any(Object));
|
|
34
34
|
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://hq.sinajs.cn/list=gb_AAPL', expect.any(Object));
|
|
@@ -48,7 +48,7 @@ describe('sinafinance stock command', () => {
|
|
|
48
48
|
.mockResolvedValueOnce(textResponse('var hq_str_gb_AAPL="苹果公司,189.98,1.23,0,1.56,0,188.50,180.00,195.00,175.00,1200000,0,3000000000000";'));
|
|
49
49
|
vi.stubGlobal('fetch', fetchMock);
|
|
50
50
|
|
|
51
|
-
const result = await cmd.func(
|
|
51
|
+
const result = await cmd.func({ key: '苹果', market: 'auto' });
|
|
52
52
|
|
|
53
53
|
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://hq.sinajs.cn/list=gb_AAPL', expect.any(Object));
|
|
54
54
|
expect(result[0]).toMatchObject({
|
package/clis/spotify/spotify.js
CHANGED
|
@@ -198,7 +198,7 @@ cli({
|
|
|
198
198
|
browser: false,
|
|
199
199
|
args: [{ name: 'query', type: 'str', default: '', positional: true, help: 'Track or artist to play (optional)' }],
|
|
200
200
|
columns: ['track', 'artist', 'status'],
|
|
201
|
-
func: async (
|
|
201
|
+
func: async (kwargs) => {
|
|
202
202
|
if (kwargs.query) {
|
|
203
203
|
const { uri, name, artist } = await findTrackUri(kwargs.query);
|
|
204
204
|
await api('PUT', '/me/player/play', { uris: [uri] });
|
|
@@ -246,7 +246,7 @@ cli({
|
|
|
246
246
|
browser: false,
|
|
247
247
|
args: [{ name: 'level', type: 'int', default: 50, positional: true, required: true, help: 'Volume 0–100' }],
|
|
248
248
|
columns: ['volume'],
|
|
249
|
-
func: async (
|
|
249
|
+
func: async (kwargs) => {
|
|
250
250
|
const level = Math.round(kwargs.level);
|
|
251
251
|
if (level < 0 || level > 100)
|
|
252
252
|
throw new CliError('INVALID_ARGS', 'Volume must be between 0 and 100');
|
|
@@ -265,7 +265,7 @@ cli({
|
|
|
265
265
|
{ name: 'limit', type: 'int', default: 10, help: 'Number of results (default: 10)' },
|
|
266
266
|
],
|
|
267
267
|
columns: ['track', 'artist', 'album', 'uri'],
|
|
268
|
-
func: async (
|
|
268
|
+
func: async (kwargs) => {
|
|
269
269
|
const limit = Math.min(50, Math.max(1, Math.round(kwargs.limit)));
|
|
270
270
|
const data = await api('GET', `/search?q=${encodeURIComponent(kwargs.query)}&type=track&limit=${limit}`);
|
|
271
271
|
const results = mapSpotifyTrackResults(data);
|
|
@@ -282,7 +282,7 @@ cli({
|
|
|
282
282
|
browser: false,
|
|
283
283
|
args: [{ name: 'query', type: 'str', required: true, positional: true, help: 'Track to add to queue' }],
|
|
284
284
|
columns: ['track', 'artist', 'status'],
|
|
285
|
-
func: async (
|
|
285
|
+
func: async (kwargs) => {
|
|
286
286
|
const { uri, name, artist } = await findTrackUri(kwargs.query);
|
|
287
287
|
await api('POST', `/me/player/queue?uri=${encodeURIComponent(uri)}`);
|
|
288
288
|
return [{ track: name, artist, status: 'added to queue' }];
|
|
@@ -296,7 +296,7 @@ cli({
|
|
|
296
296
|
browser: false,
|
|
297
297
|
args: [{ name: 'state', type: 'str', default: 'on', positional: true, choices: ['on', 'off'], help: 'on or off' }],
|
|
298
298
|
columns: ['shuffle'],
|
|
299
|
-
func: async (
|
|
299
|
+
func: async (kwargs) => {
|
|
300
300
|
await api('PUT', `/me/player/shuffle?state=${kwargs.state === 'on'}`);
|
|
301
301
|
return [{ shuffle: kwargs.state }];
|
|
302
302
|
},
|
|
@@ -309,7 +309,7 @@ cli({
|
|
|
309
309
|
browser: false,
|
|
310
310
|
args: [{ name: 'mode', type: 'str', default: 'context', positional: true, choices: ['off', 'track', 'context'], help: 'off / track / context' }],
|
|
311
311
|
columns: ['repeat'],
|
|
312
|
-
func: async (
|
|
312
|
+
func: async (kwargs) => {
|
|
313
313
|
await api('PUT', `/me/player/repeat?state=${kwargs.mode}`);
|
|
314
314
|
return [{ repeat: kwargs.mode }];
|
|
315
315
|
},
|
package/clis/substack/search.js
CHANGED
|
@@ -69,7 +69,7 @@ cli({
|
|
|
69
69
|
{ name: 'limit', type: 'int', default: 20, help: '返回结果数量' },
|
|
70
70
|
],
|
|
71
71
|
columns: ['rank', 'title', 'author', 'date', 'description', 'url'],
|
|
72
|
-
func: async (
|
|
72
|
+
func: async (args) => {
|
|
73
73
|
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 50));
|
|
74
74
|
return args.type === 'publications'
|
|
75
75
|
? searchPublications(args.keyword, limit)
|
package/clis/toutiao/articles.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { cli } from '@jackwener/opencli/registry';
|
|
2
2
|
|
|
3
|
-
const NON_TITLE_LINES = new Set([
|
|
4
|
-
'展现', '阅读', '点赞', '评论',
|
|
5
|
-
'查看数据', '查看评论', '修改', '更多', '首发',
|
|
6
|
-
'已发布', '定时发布', '定时发布中', '由文章生成', '审核中',
|
|
7
|
-
]);
|
|
8
|
-
|
|
9
3
|
export function parseToutiaoArticlesText(text) {
|
|
4
|
+
const NON_TITLE_LINES = new Set([
|
|
5
|
+
'展现', '阅读', '点赞', '评论',
|
|
6
|
+
'查看数据', '查看评论', '修改', '更多', '首发',
|
|
7
|
+
'已发布', '定时发布', '定时发布中', '由文章生成', '审核中',
|
|
8
|
+
]);
|
|
10
9
|
const lines = String(text || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
11
10
|
const results = [];
|
|
12
11
|
|
|
@@ -2,22 +2,29 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { __test__ } from './articles.js';
|
|
3
3
|
|
|
4
4
|
describe('toutiao articles parser', () => {
|
|
5
|
+
const articleText = [
|
|
6
|
+
'短标题',
|
|
7
|
+
'04-20 20:30',
|
|
8
|
+
'已发布',
|
|
9
|
+
'展现 8 阅读 0 点赞 0 评论 0',
|
|
10
|
+
].join('\n');
|
|
11
|
+
const parsedArticle = {
|
|
12
|
+
title: '短标题',
|
|
13
|
+
date: '04-20 20:30',
|
|
14
|
+
status: '已发布',
|
|
15
|
+
'展现': '8',
|
|
16
|
+
'阅读': '0',
|
|
17
|
+
'点赞': '0',
|
|
18
|
+
'评论': '0',
|
|
19
|
+
};
|
|
20
|
+
|
|
5
21
|
it('keeps short chinese titles instead of silently dropping the row', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
].join('\n');
|
|
22
|
+
expect(__test__.parseToutiaoArticlesText(articleText)).toEqual([parsedArticle]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps parsing when serialized into the browser evaluate context', () => {
|
|
26
|
+
const parse = Function(`return (${__test__.parseToutiaoArticlesText.toString()})`)();
|
|
12
27
|
|
|
13
|
-
expect(
|
|
14
|
-
title: '短标题',
|
|
15
|
-
date: '04-20 20:30',
|
|
16
|
-
status: '已发布',
|
|
17
|
-
'展现': '8',
|
|
18
|
-
'阅读': '0',
|
|
19
|
-
'点赞': '0',
|
|
20
|
-
'评论': '0',
|
|
21
|
-
}]);
|
|
28
|
+
expect(parse(articleText)).toEqual([parsedArticle]);
|
|
22
29
|
});
|
|
23
30
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthRequiredError,
|
|
1
|
+
import { AuthRequiredError, selectorError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
3
|
cli({
|
|
4
4
|
site: 'twitter',
|
|
@@ -49,7 +49,7 @@ cli({
|
|
|
49
49
|
return false;
|
|
50
50
|
}`);
|
|
51
51
|
if (!clicked) {
|
|
52
|
-
throw
|
|
52
|
+
throw selectorError('Twitter followers link', 'Twitter may have changed the layout.');
|
|
53
53
|
}
|
|
54
54
|
await page.waitForCapture(5);
|
|
55
55
|
// 4. Scroll to trigger pagination API calls
|
|
@@ -1,11 +1,142 @@
|
|
|
1
|
-
import { AuthRequiredError, SelectorError } from '@jackwener/opencli/errors';
|
|
2
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { resolveTwitterQueryId, sanitizeQueryId } from './shared.js';
|
|
4
|
+
|
|
5
|
+
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
6
|
+
const FOLLOWING_QUERY_ID = 'zx6e-TLzRkeDO_a7p4b3JQ'; // Following fallback
|
|
7
|
+
const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
|
|
8
|
+
|
|
9
|
+
const FEATURES = {
|
|
10
|
+
rweb_video_screen_enabled: false,
|
|
11
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
12
|
+
responsive_web_profile_redirect_enabled: false,
|
|
13
|
+
rweb_tipjar_consumption_enabled: false,
|
|
14
|
+
verified_phone_label_enabled: false,
|
|
15
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
16
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
17
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
18
|
+
premium_content_api_read_enabled: false,
|
|
19
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
20
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
21
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
22
|
+
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
23
|
+
responsive_web_jetfuel_frame: true,
|
|
24
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
25
|
+
responsive_web_grok_annotations_enabled: true,
|
|
26
|
+
articles_preview_enabled: true,
|
|
27
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
28
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
29
|
+
view_counts_everywhere_api_enabled: true,
|
|
30
|
+
longform_notetweets_consumption_enabled: true,
|
|
31
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
32
|
+
tweet_awards_web_tipping_enabled: false,
|
|
33
|
+
content_disclosure_indicator_enabled: true,
|
|
34
|
+
content_disclosure_ai_generated_indicator_enabled: true,
|
|
35
|
+
responsive_web_grok_show_grok_translated_post: false,
|
|
36
|
+
responsive_web_grok_analysis_button_from_backend: true,
|
|
37
|
+
post_ctas_fetch_enabled: false,
|
|
38
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
39
|
+
standardized_nudges_misinfo: true,
|
|
40
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
41
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
42
|
+
longform_notetweets_inline_media_enabled: false,
|
|
43
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
44
|
+
responsive_web_grok_imagine_annotation_enabled: true,
|
|
45
|
+
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
|
46
|
+
responsive_web_enhance_cards_enabled: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function buildFollowingUrl(queryId, userId, count, cursor) {
|
|
50
|
+
const vars = {
|
|
51
|
+
userId,
|
|
52
|
+
count,
|
|
53
|
+
includePromotedContent: false,
|
|
54
|
+
withClientEventToken: false,
|
|
55
|
+
withBirdwatchNotes: false,
|
|
56
|
+
withVoice: true,
|
|
57
|
+
withV2Timeline: true,
|
|
58
|
+
};
|
|
59
|
+
if (cursor)
|
|
60
|
+
vars.cursor = cursor;
|
|
61
|
+
return `/i/api/graphql/${queryId}/Following`
|
|
62
|
+
+ `?variables=${encodeURIComponent(JSON.stringify(vars))}`
|
|
63
|
+
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildUserByScreenNameUrl(queryId, screenName) {
|
|
67
|
+
const vars = JSON.stringify({ screen_name: screenName, withSafetyModeUserFields: true });
|
|
68
|
+
const feats = JSON.stringify({
|
|
69
|
+
hidden_profile_subscriptions_enabled: true,
|
|
70
|
+
rweb_tipjar_consumption_enabled: true,
|
|
71
|
+
responsive_web_graphql_exclude_directive_enabled: true,
|
|
72
|
+
verified_phone_label_enabled: false,
|
|
73
|
+
subscriptions_verification_info_is_identity_verified_enabled: true,
|
|
74
|
+
subscriptions_verification_info_verified_since_enabled: true,
|
|
75
|
+
highlights_tweets_tab_ui_enabled: true,
|
|
76
|
+
responsive_web_twitter_article_notes_tab_enabled: true,
|
|
77
|
+
subscriptions_feature_can_gift_premium: true,
|
|
78
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
79
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
80
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
81
|
+
});
|
|
82
|
+
return `/i/api/graphql/${queryId}/UserByScreenName`
|
|
83
|
+
+ `?variables=${encodeURIComponent(vars)}`
|
|
84
|
+
+ `&features=${encodeURIComponent(feats)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractUser(result) {
|
|
88
|
+
if (!result || result.__typename !== 'User')
|
|
89
|
+
return null;
|
|
90
|
+
const core = result.core || {};
|
|
91
|
+
const legacy = result.legacy || {};
|
|
92
|
+
return {
|
|
93
|
+
screen_name: core.screen_name || legacy.screen_name || 'unknown',
|
|
94
|
+
name: core.name || legacy.name || 'unknown',
|
|
95
|
+
bio: legacy.description || result.profile_bio?.description || '',
|
|
96
|
+
followers: legacy.followers_count || legacy.normal_followers_count || 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseFollowing(data) {
|
|
101
|
+
const users = [];
|
|
102
|
+
let nextCursor = null;
|
|
103
|
+
const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions
|
|
104
|
+
|| data?.data?.user?.result?.timeline?.timeline?.instructions
|
|
105
|
+
|| [];
|
|
106
|
+
for (const inst of instructions) {
|
|
107
|
+
for (const entry of inst.entries || []) {
|
|
108
|
+
const content = entry.content;
|
|
109
|
+
// Extract cursor
|
|
110
|
+
if (content?.entryType === 'TimelineTimelineCursor' || content?.__typename === 'TimelineTimelineCursor') {
|
|
111
|
+
if (content.cursorType === 'Bottom' || content.cursorType === 'ShowMore')
|
|
112
|
+
nextCursor = content.value;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.entryId?.startsWith('cursor-bottom-') || entry.entryId?.startsWith('cursor-showMore-')) {
|
|
116
|
+
nextCursor = content?.value || content?.itemContent?.value || nextCursor;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Extract user
|
|
120
|
+
if (entry.entryId?.startsWith('user-')) {
|
|
121
|
+
const user = extractUser(content?.itemContent?.user_results?.result);
|
|
122
|
+
if (user)
|
|
123
|
+
users.push(user);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { users, nextCursor };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeScreenName(value) {
|
|
131
|
+
return String(value || '').trim().replace(/^\/+/, '').replace(/^@+/, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
3
134
|
cli({
|
|
4
135
|
site: 'twitter',
|
|
5
136
|
name: 'following',
|
|
6
137
|
description: 'Get accounts a Twitter/X user is following',
|
|
7
138
|
domain: 'x.com',
|
|
8
|
-
strategy: Strategy.
|
|
139
|
+
strategy: Strategy.COOKIE,
|
|
9
140
|
browser: true,
|
|
10
141
|
args: [
|
|
11
142
|
{ name: 'user', positional: true, type: 'string', required: false },
|
|
@@ -13,83 +144,103 @@ cli({
|
|
|
13
144
|
],
|
|
14
145
|
columns: ['screen_name', 'name', 'bio', 'followers'],
|
|
15
146
|
func: async (page, kwargs) => {
|
|
16
|
-
|
|
17
|
-
|
|
147
|
+
const limit = kwargs.limit === undefined || kwargs.limit === null ? 50 : Number(kwargs.limit);
|
|
148
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
149
|
+
throw new ArgumentError('twitter following --limit must be a positive integer', 'Example: opencli twitter following @elonmusk --limit 200');
|
|
150
|
+
}
|
|
151
|
+
let targetUser = normalizeScreenName(kwargs.user);
|
|
152
|
+
|
|
153
|
+
await page.goto('https://x.com');
|
|
154
|
+
await page.wait(3);
|
|
155
|
+
|
|
156
|
+
const ct0 = await page.evaluate(`() => {
|
|
157
|
+
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
|
|
158
|
+
}`);
|
|
159
|
+
if (!ct0)
|
|
160
|
+
throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
|
|
161
|
+
|
|
18
162
|
if (!targetUser) {
|
|
19
|
-
await page.goto('https://x.com/home');
|
|
20
|
-
await page.wait({ selector: '[data-testid="primaryColumn"]' });
|
|
21
163
|
const href = await page.evaluate(`() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!href)
|
|
26
|
-
throw new AuthRequiredError('x.com', 'Could not
|
|
27
|
-
|
|
28
|
-
targetUser = href.replace('/', '');
|
|
164
|
+
const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
|
|
165
|
+
return link ? link.getAttribute('href') : null;
|
|
166
|
+
}`);
|
|
167
|
+
if (!href)
|
|
168
|
+
throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?');
|
|
169
|
+
targetUser = normalizeScreenName(href.replace('/', ''));
|
|
29
170
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await page.wait(3);
|
|
33
|
-
// 2. Install interceptor BEFORE SPA navigation.
|
|
34
|
-
// goto() resets JS context, but SPA click preserves it.
|
|
35
|
-
await page.installInterceptor('Following');
|
|
36
|
-
// 3. Click the following link via SPA navigation (preserves interceptor)
|
|
37
|
-
const safeUser = JSON.stringify(targetUser);
|
|
38
|
-
const clicked = await page.evaluate(`() => {
|
|
39
|
-
const target = ${safeUser};
|
|
40
|
-
const link = document.querySelector('a[href="/' + target + '/following"]');
|
|
41
|
-
if (link) { link.click(); return true; }
|
|
42
|
-
return false;
|
|
43
|
-
}`);
|
|
44
|
-
if (!clicked) {
|
|
45
|
-
throw new SelectorError('Twitter following link', 'Twitter may have changed the layout.');
|
|
171
|
+
if (!targetUser) {
|
|
172
|
+
throw new ArgumentError('twitter following user cannot be empty', 'Example: opencli twitter following @elonmusk --limit 200');
|
|
46
173
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
174
|
+
|
|
175
|
+
const followingQueryId = await resolveTwitterQueryId(page, 'Following', FOLLOWING_QUERY_ID);
|
|
176
|
+
const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
|
|
177
|
+
const headers = JSON.stringify({
|
|
178
|
+
'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
|
|
179
|
+
'X-Csrf-Token': ct0,
|
|
180
|
+
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
181
|
+
'X-Twitter-Active-User': 'yes',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Get userId from screen_name
|
|
185
|
+
const userLookup = await page.evaluate(`async () => {
|
|
186
|
+
const url = ${JSON.stringify(buildUserByScreenNameUrl(userByScreenNameQueryId, targetUser))};
|
|
187
|
+
const resp = await fetch(url, { headers: ${headers}, credentials: 'include' });
|
|
188
|
+
if (!resp.ok) return { error: resp.status };
|
|
189
|
+
const d = await resp.json();
|
|
190
|
+
return { userId: d.data?.user?.result?.rest_id || null };
|
|
191
|
+
}`);
|
|
192
|
+
if (userLookup?.error === 401 || userLookup?.error === 403) {
|
|
193
|
+
throw new AuthRequiredError('x.com', `Twitter user lookup failed (HTTP ${userLookup.error})`);
|
|
55
194
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name: core.name || legacy.name || 'unknown',
|
|
80
|
-
bio: legacy.description || item.profile_bio?.description || '',
|
|
81
|
-
followers: legacy.followers_count || legacy.normal_followers_count || 0
|
|
82
|
-
});
|
|
83
|
-
}
|
|
195
|
+
if (userLookup?.error) {
|
|
196
|
+
throw new CommandExecutionError(`HTTP ${userLookup.error}: Failed to resolve Twitter user @${targetUser}`);
|
|
197
|
+
}
|
|
198
|
+
const userId = userLookup?.userId || null;
|
|
199
|
+
if (!userId)
|
|
200
|
+
throw new CommandExecutionError(`Could not find user @${targetUser}`);
|
|
201
|
+
|
|
202
|
+
const allUsers = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
let cursor = null;
|
|
205
|
+
|
|
206
|
+
const maxPages = Math.ceil(limit / 50) + 2;
|
|
207
|
+
for (let i = 0; i < maxPages && allUsers.length < limit; i++) {
|
|
208
|
+
const fetchCount = Math.min(50, limit - allUsers.length + 10);
|
|
209
|
+
const apiUrl = buildFollowingUrl(followingQueryId, userId, fetchCount, cursor);
|
|
210
|
+
const data = await page.evaluate(`async () => {
|
|
211
|
+
const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
|
|
212
|
+
return r.ok ? await r.json() : { error: r.status };
|
|
213
|
+
}`);
|
|
214
|
+
if (data?.error) {
|
|
215
|
+
if (data.error === 401 || data.error === 403)
|
|
216
|
+
throw new AuthRequiredError('x.com', `Twitter following request failed (HTTP ${data.error})`);
|
|
217
|
+
throw new CommandExecutionError(`HTTP ${data.error}: Failed to fetch following list. queryId may have expired.`);
|
|
84
218
|
}
|
|
85
|
-
|
|
86
|
-
|
|
219
|
+
const { users, nextCursor } = parseFollowing(data);
|
|
220
|
+
for (const u of users) {
|
|
221
|
+
if (!seen.has(u.screen_name)) {
|
|
222
|
+
seen.add(u.screen_name);
|
|
223
|
+
allUsers.push(u);
|
|
224
|
+
}
|
|
87
225
|
}
|
|
226
|
+
if (!nextCursor || nextCursor === cursor)
|
|
227
|
+
break;
|
|
228
|
+
cursor = nextCursor;
|
|
88
229
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
230
|
+
|
|
231
|
+
if (allUsers.length === 0) {
|
|
232
|
+
throw new EmptyResultError('twitter following', `No following accounts found for @${targetUser}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return allUsers.slice(0, limit);
|
|
236
|
+
},
|
|
95
237
|
});
|
|
238
|
+
|
|
239
|
+
export const __test__ = {
|
|
240
|
+
sanitizeQueryId,
|
|
241
|
+
buildFollowingUrl,
|
|
242
|
+
buildUserByScreenNameUrl,
|
|
243
|
+
extractUser,
|
|
244
|
+
normalizeScreenName,
|
|
245
|
+
parseFollowing,
|
|
246
|
+
};
|