@jackwener/opencli 1.7.7 → 1.7.9
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 +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- 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 +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -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 +3 -22
- 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 +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- 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/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/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- 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 +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- 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/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -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 -0
- package/dist/src/browser/bridge.js +36 -9
- 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/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 +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- 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 +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -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/36kr/news.js
CHANGED
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of articles (max 50)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['rank', 'title', 'summary', 'date', 'url'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (kwargs) => {
|
|
16
16
|
const count = Math.min(kwargs.limit || 20, 50);
|
|
17
17
|
const resp = await fetch('https://www.36kr.com/feed', {
|
|
18
18
|
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; opencli/1.0)' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
1
|
+
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
-
import { buildDiscussionUrl, buildProvenance, cleanText, extractAsin, normalizeProductUrl, parseRatingValue, parseReviewCount, trimRatingPrefix, uniqueNonEmpty, assertUsableState, gotoAndReadState, } from './shared.js';
|
|
3
|
+
import { buildProductUrl, buildDiscussionUrl, buildProvenance, cleanText, extractAsin, normalizeProductUrl, parseRatingValue, parseReviewCount, trimRatingPrefix, uniqueNonEmpty, assertUsableState, gotoAndReadState, } from './shared.js';
|
|
4
4
|
function normalizeDiscussionPayload(payload) {
|
|
5
5
|
const sourceUrl = cleanText(payload.href) || buildDiscussionUrl(payload.href ?? '');
|
|
6
6
|
const asin = extractAsin(payload.href ?? '') ?? null;
|
|
@@ -28,10 +28,16 @@ function normalizeDiscussionPayload(payload) {
|
|
|
28
28
|
})),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
function hasDiscussionSummary(payload) {
|
|
32
|
+
return Boolean(cleanText(payload.average_rating_text) || cleanText(payload.total_review_count_text));
|
|
33
|
+
}
|
|
34
|
+
function isSignInState(state) {
|
|
35
|
+
const href = cleanText(state.href).toLowerCase();
|
|
36
|
+
const title = cleanText(state.title).toLowerCase();
|
|
37
|
+
return href.includes('/ap/signin')
|
|
38
|
+
|| title.includes('amazon sign-in');
|
|
39
|
+
}
|
|
40
|
+
async function readCurrentDiscussionPayload(page, limit) {
|
|
35
41
|
return await page.evaluate(`
|
|
36
42
|
(() => ({
|
|
37
43
|
href: window.location.href,
|
|
@@ -53,6 +59,29 @@ async function readDiscussionPayload(page, input, limit) {
|
|
|
53
59
|
}))()
|
|
54
60
|
`);
|
|
55
61
|
}
|
|
62
|
+
async function readDiscussionPayload(page, input, limit) {
|
|
63
|
+
const reviewUrl = buildDiscussionUrl(input);
|
|
64
|
+
const reviewState = await gotoAndReadState(page, reviewUrl, 2500, 'discussion');
|
|
65
|
+
assertUsableState(reviewState, 'discussion');
|
|
66
|
+
const reviewPayload = await readCurrentDiscussionPayload(page, limit);
|
|
67
|
+
if (hasDiscussionSummary(reviewPayload)) {
|
|
68
|
+
return reviewPayload;
|
|
69
|
+
}
|
|
70
|
+
const productUrl = buildProductUrl(input);
|
|
71
|
+
const productState = await gotoAndReadState(page, productUrl, 2500, 'discussion');
|
|
72
|
+
assertUsableState(productState, 'discussion');
|
|
73
|
+
if (isSignInState(reviewState) && isSignInState(productState)) {
|
|
74
|
+
throw new AuthRequiredError('amazon.com', 'Amazon review discussion requires an active signed-in Amazon session in the shared Chrome profile.');
|
|
75
|
+
}
|
|
76
|
+
const productPayload = await readCurrentDiscussionPayload(page, limit);
|
|
77
|
+
if (hasDiscussionSummary(productPayload)) {
|
|
78
|
+
return productPayload;
|
|
79
|
+
}
|
|
80
|
+
if (isSignInState(reviewState)) {
|
|
81
|
+
throw new CommandExecutionError('amazon review page redirected to sign-in and product page fallback did not expose review summary', 'Open the product page in Chrome, verify reviews are visible, and retry.');
|
|
82
|
+
}
|
|
83
|
+
return reviewPayload;
|
|
84
|
+
}
|
|
56
85
|
cli({
|
|
57
86
|
site: 'amazon',
|
|
58
87
|
name: 'discussion',
|
|
@@ -88,4 +117,6 @@ cli({
|
|
|
88
117
|
});
|
|
89
118
|
export const __test__ = {
|
|
90
119
|
normalizeDiscussionPayload,
|
|
120
|
+
hasDiscussionSummary,
|
|
121
|
+
isSignInState,
|
|
91
122
|
};
|
|
@@ -1,36 +1,151 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
2
4
|
import { __test__ } from './discussion.js';
|
|
5
|
+
import './discussion.js';
|
|
6
|
+
|
|
7
|
+
function createPageMock(evaluateResults) {
|
|
8
|
+
const evaluate = vi.fn();
|
|
9
|
+
for (const result of evaluateResults) {
|
|
10
|
+
evaluate.mockResolvedValueOnce(result);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate,
|
|
16
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
|
|
22
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
23
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
25
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
26
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
30
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
31
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
32
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
3
36
|
describe('amazon discussion normalization', () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
expect(result.asin).toBe('B0FJS72893');
|
|
22
|
-
expect(result.average_rating_value).toBe(3.9);
|
|
23
|
-
expect(result.total_review_count).toBe(27);
|
|
24
|
-
expect(result.review_samples).toEqual([
|
|
25
|
-
{
|
|
26
|
-
title: 'Great value and quality',
|
|
27
|
-
rating_text: '5.0 out of 5 stars',
|
|
28
|
-
rating_value: 5,
|
|
29
|
-
author: 'GTreader2',
|
|
30
|
-
date_text: 'Reviewed in the United States on February 21, 2026',
|
|
31
|
-
body: 'Small but mighty.',
|
|
32
|
-
verified_purchase: true,
|
|
33
|
-
},
|
|
34
|
-
]);
|
|
37
|
+
it('normalizes review summary and sample reviews', () => {
|
|
38
|
+
const result = __test__.normalizeDiscussionPayload({
|
|
39
|
+
href: 'https://www.amazon.com/product-reviews/B0FJS72893',
|
|
40
|
+
average_rating_text: '3.9 out of 5',
|
|
41
|
+
total_review_count_text: '27 global ratings',
|
|
42
|
+
qa_links: [],
|
|
43
|
+
review_samples: [
|
|
44
|
+
{
|
|
45
|
+
title: '5.0 out of 5 stars Great value and quality',
|
|
46
|
+
rating_text: '5.0 out of 5 stars',
|
|
47
|
+
author: 'GTreader2',
|
|
48
|
+
date_text: 'Reviewed in the United States on February 21, 2026',
|
|
49
|
+
body: 'Small but mighty.',
|
|
50
|
+
verified: true,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
35
53
|
});
|
|
54
|
+
|
|
55
|
+
expect(result.asin).toBe('B0FJS72893');
|
|
56
|
+
expect(result.average_rating_value).toBe(3.9);
|
|
57
|
+
expect(result.total_review_count).toBe(27);
|
|
58
|
+
expect(result.review_samples).toEqual([
|
|
59
|
+
{
|
|
60
|
+
title: 'Great value and quality',
|
|
61
|
+
rating_text: '5.0 out of 5 stars',
|
|
62
|
+
rating_value: 5,
|
|
63
|
+
author: 'GTreader2',
|
|
64
|
+
date_text: 'Reviewed in the United States on February 21, 2026',
|
|
65
|
+
body: 'Small but mighty.',
|
|
66
|
+
verified_purchase: true,
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('falls back to the product page when the review page redirects to sign-in', async () => {
|
|
72
|
+
const command = getRegistry().get('amazon/discussion');
|
|
73
|
+
const page = createPageMock([
|
|
74
|
+
{
|
|
75
|
+
href: 'https://www.amazon.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.amazon.com%2Fproduct-reviews%2FB09HKN2ZRT',
|
|
76
|
+
title: 'Amazon Sign-In',
|
|
77
|
+
body_text: 'Sign in Create account',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
href: 'https://www.amazon.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.amazon.com%2Fproduct-reviews%2FB09HKN2ZRT',
|
|
81
|
+
average_rating_text: '',
|
|
82
|
+
total_review_count_text: '',
|
|
83
|
+
review_samples: [],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
href: 'https://www.amazon.com/dp/B09HKN2ZRT',
|
|
87
|
+
title: 'Amazon.com: Example product',
|
|
88
|
+
body_text: 'Hello, zejia-wu Reviews',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
href: 'https://www.amazon.com/dp/B09HKN2ZRT',
|
|
92
|
+
average_rating_text: '4.4 out of 5',
|
|
93
|
+
total_review_count_text: '349 global ratings',
|
|
94
|
+
review_samples: [
|
|
95
|
+
{
|
|
96
|
+
title: '5.0 out of 5 stars Perfect for the office',
|
|
97
|
+
rating_text: '5.0 out of 5 stars',
|
|
98
|
+
author: 'Ken',
|
|
99
|
+
date_text: 'Reviewed in the United States on March 19, 2026',
|
|
100
|
+
body: 'Good for the office, no complaints.',
|
|
101
|
+
verified: true,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const result = await command.func(page, { input: 'B09HKN2ZRT', limit: 1 });
|
|
108
|
+
|
|
109
|
+
expect(page.goto.mock.calls.map((call) => call[0])).toEqual([
|
|
110
|
+
'https://www.amazon.com/product-reviews/B09HKN2ZRT',
|
|
111
|
+
'https://www.amazon.com/dp/B09HKN2ZRT',
|
|
112
|
+
]);
|
|
113
|
+
expect(result).toEqual([
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
asin: 'B09HKN2ZRT',
|
|
116
|
+
discussion_url: 'https://www.amazon.com/dp/B09HKN2ZRT',
|
|
117
|
+
average_rating_value: 4.4,
|
|
118
|
+
total_review_count: 349,
|
|
119
|
+
}),
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('throws AuthRequiredError when both review and product pages are gated', async () => {
|
|
124
|
+
const command = getRegistry().get('amazon/discussion');
|
|
125
|
+
const authState = {
|
|
126
|
+
href: 'https://www.amazon.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.amazon.com%2Fproduct-reviews%2FB09HKN2ZRT',
|
|
127
|
+
title: 'Amazon Sign-In',
|
|
128
|
+
body_text: 'Sign in Create account',
|
|
129
|
+
};
|
|
130
|
+
const page = createPageMock([
|
|
131
|
+
authState,
|
|
132
|
+
{
|
|
133
|
+
href: authState.href,
|
|
134
|
+
average_rating_text: '',
|
|
135
|
+
total_review_count_text: '',
|
|
136
|
+
review_samples: [],
|
|
137
|
+
},
|
|
138
|
+
authState,
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
await expect(command.func(page, { input: 'B09HKN2ZRT', limit: 1 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not treat a public product page with sign-in copy as a gated page', () => {
|
|
145
|
+
expect(__test__.isSignInState({
|
|
146
|
+
href: 'https://www.amazon.com/dp/B09HKN2ZRT',
|
|
147
|
+
title: 'Amazon.com: Example product',
|
|
148
|
+
body_text: 'Hello, sign in Account & Lists Create account',
|
|
149
|
+
})).toBe(false);
|
|
150
|
+
});
|
|
36
151
|
});
|
|
@@ -24,7 +24,7 @@ describe('apple-podcasts search command', () => {
|
|
|
24
24
|
}),
|
|
25
25
|
});
|
|
26
26
|
vi.stubGlobal('fetch', fetchMock);
|
|
27
|
-
const result = await cmd.func(
|
|
27
|
+
const result = await cmd.func({
|
|
28
28
|
query: 'machine learning',
|
|
29
29
|
keyword: 'sports',
|
|
30
30
|
limit: 5,
|
|
@@ -60,7 +60,7 @@ describe('apple-podcasts top command', () => {
|
|
|
60
60
|
}),
|
|
61
61
|
});
|
|
62
62
|
vi.stubGlobal('fetch', fetchMock);
|
|
63
|
-
await cmd.func(
|
|
63
|
+
await cmd.func({ country: 'US', limit: 1 });
|
|
64
64
|
const [, options] = fetchMock.mock.calls[0] ?? [];
|
|
65
65
|
expect(options).toBeDefined();
|
|
66
66
|
expect(options.signal).toBeDefined();
|
|
@@ -81,7 +81,7 @@ describe('apple-podcasts top command', () => {
|
|
|
81
81
|
}),
|
|
82
82
|
});
|
|
83
83
|
vi.stubGlobal('fetch', fetchMock);
|
|
84
|
-
const result = await cmd.func(
|
|
84
|
+
const result = await cmd.func({ country: 'US', limit: 2 });
|
|
85
85
|
expect(fetchMock).toHaveBeenCalledWith('https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json', expect.objectContaining({
|
|
86
86
|
signal: expect.any(Object),
|
|
87
87
|
}));
|
|
@@ -94,6 +94,6 @@ describe('apple-podcasts top command', () => {
|
|
|
94
94
|
const cmd = getRegistry().get('apple-podcasts/top');
|
|
95
95
|
expect(cmd?.func).toBeTypeOf('function');
|
|
96
96
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
|
|
97
|
-
await expect(cmd.func(
|
|
97
|
+
await expect(cmd.func({ country: 'us', limit: 3 })).rejects.toThrow('Unable to reach Apple Podcasts charts for US');
|
|
98
98
|
});
|
|
99
99
|
});
|
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'limit', type: 'int', default: 15, help: 'Max episodes to show' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['title', 'duration', 'date'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (args) => {
|
|
16
16
|
const limit = Math.max(1, Math.min(Number(args.limit), 200));
|
|
17
17
|
// results[0] is the podcast itself; the rest are episodes
|
|
18
18
|
const data = await itunesFetch(`/lookup?id=${args.id}&entity=podcastEpisode&limit=${limit + 1}`);
|
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['id', 'title', 'author', 'episodes', 'genre', 'url'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (args) => {
|
|
16
16
|
const term = encodeURIComponent(args.query);
|
|
17
17
|
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
18
18
|
const data = await itunesFetch(`/search?term=${term}&media=podcast&limit=${limit}`);
|
|
@@ -14,7 +14,7 @@ cli({
|
|
|
14
14
|
{ name: 'country', default: 'us', help: 'Country code (e.g. us, cn, gb, jp)' },
|
|
15
15
|
],
|
|
16
16
|
columns: ['rank', 'title', 'author', 'id'],
|
|
17
|
-
func: async (
|
|
17
|
+
func: async (args) => {
|
|
18
18
|
const limit = Math.max(1, Math.min(Number(args.limit), 100));
|
|
19
19
|
const country = String(args.country || 'us').trim().toLowerCase();
|
|
20
20
|
const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
|
package/clis/arxiv/paper.js
CHANGED
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'id', positional: true, required: true, help: 'arXiv paper ID (e.g. 1706.03762)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['id', 'title', 'authors', 'published', 'abstract', 'url'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (args) => {
|
|
15
15
|
const xml = await arxivFetch(`id_list=${encodeURIComponent(args.id)}`);
|
|
16
16
|
const entries = parseEntries(xml);
|
|
17
17
|
if (!entries.length)
|
package/clis/arxiv/search.js
CHANGED
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['id', 'title', 'authors', 'published', 'url'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (args) => {
|
|
16
16
|
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
|
17
17
|
const query = encodeURIComponent(`all:${args.query}`);
|
|
18
18
|
const xml = await arxivFetch(`search_query=${query}&max_results=${limit}&sortBy=relevance`);
|
package/clis/band/mentions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthRequiredError, EmptyResultError,
|
|
1
|
+
import { AuthRequiredError, EmptyResultError, selectorError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
3
|
/**
|
|
4
4
|
* band mentions — Show Band notifications where you were @mentioned.
|
|
@@ -52,7 +52,7 @@ cli({
|
|
|
52
52
|
await page.wait(0.5);
|
|
53
53
|
}
|
|
54
54
|
if (!bellReady) {
|
|
55
|
-
throw
|
|
55
|
+
throw selectorError('button._btnWidgetIcon', 'Notification bell not found. The Band.us UI may have changed.');
|
|
56
56
|
}
|
|
57
57
|
// Poll until a capture containing result_data.news arrives, up to maxSecs seconds.
|
|
58
58
|
// getInterceptedRequests() clears the array on each call, so captures are accumulated
|
|
@@ -80,7 +80,7 @@ cli({
|
|
|
80
80
|
return true;
|
|
81
81
|
}`);
|
|
82
82
|
if (!bellClicked) {
|
|
83
|
-
throw
|
|
83
|
+
throw selectorError('button._btnWidgetIcon', 'Notification bell disappeared before click. The Band.us UI may have changed.');
|
|
84
84
|
}
|
|
85
85
|
const requests = await waitForOneCapture();
|
|
86
86
|
// Find the get_news response (has result_data.news); get_news_count responses do not.
|
package/clis/bbc/news.js
CHANGED
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of headlines (max 50)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['rank', 'title', 'description', 'url'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (kwargs) => {
|
|
16
16
|
const count = Math.min(kwargs.limit || 20, 50);
|
|
17
17
|
const resp = await fetch('https://feeds.bbci.co.uk/news/rss.xml');
|
|
18
18
|
if (!resp.ok)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import { AuthRequiredError, CommandExecutionError, EmptyResultError,
|
|
2
|
+
import { AuthRequiredError, CommandExecutionError, EmptyResultError, selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { apiGet, resolveBvid } from './utils.js';
|
|
4
4
|
cli({
|
|
5
5
|
site: 'bilibili',
|
|
@@ -23,7 +23,7 @@ cli({
|
|
|
23
23
|
return state?.videoData?.cid;
|
|
24
24
|
})()`);
|
|
25
25
|
if (!cid) {
|
|
26
|
-
throw
|
|
26
|
+
throw selectorError('videoData.cid', '无法在页面中提取到当前视频的 CID,请检查页面是否正常加载。');
|
|
27
27
|
}
|
|
28
28
|
// 3. 在 Node 端使用 apiGet 获取带 Wbi 签名的字幕列表
|
|
29
29
|
// 之前纯靠 evaluate 里的 fetch 会失败,因为 B 站 /wbi/ 开头的接口强校验 w_rid,未签名直接被风控返回 403 HTML
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('businessweek', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('economics', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('industries', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
package/clis/bloomberg/main.js
CHANGED
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('main', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('markets', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('opinions', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('politics', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
package/clis/bloomberg/tech.js
CHANGED
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
{ name: 'limit', type: 'int', default: 1, help: 'Number of feed items to return (max 20)' },
|
|
12
12
|
],
|
|
13
13
|
columns: ['title', 'summary', 'link', 'mediaLinks'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (kwargs) => {
|
|
15
15
|
return fetchBloombergFeed('tech', kwargs.limit ?? 1);
|
|
16
16
|
},
|
|
17
17
|
});
|
package/clis/boss/search.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* BOSS直聘 job search — browser cookie API.
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
5
6
|
import { requirePage, navigateTo, bossFetch, verbose } from './utils.js';
|
|
6
7
|
/** City name → BOSS Zhipin city code mapping */
|
|
7
8
|
const CITY_CODES = {
|
|
@@ -22,8 +23,16 @@ const CITY_CODES = {
|
|
|
22
23
|
'香港': '101320100',
|
|
23
24
|
};
|
|
24
25
|
const EXP_MAP = {
|
|
25
|
-
'不限': '0',
|
|
26
|
-
'
|
|
26
|
+
'不限': '0',
|
|
27
|
+
'在校/应届': '108',
|
|
28
|
+
'在校生': '108', '在校': '108',
|
|
29
|
+
'应届生': '102', '应届': '102',
|
|
30
|
+
'经验不限': '101',
|
|
31
|
+
'1年以内': '103',
|
|
32
|
+
'1-3年': '104',
|
|
33
|
+
'3-5年': '105',
|
|
34
|
+
'5-10年': '106',
|
|
35
|
+
'10年以上': '107',
|
|
27
36
|
};
|
|
28
37
|
const DEGREE_MAP = {
|
|
29
38
|
'不限': '0', '初中及以下': '209', '中专/中技': '208', '高中': '206',
|
|
@@ -38,6 +47,10 @@ const INDUSTRY_MAP = {
|
|
|
38
47
|
'人工智能': '100901', '大数据': '100902', '金融': '100101',
|
|
39
48
|
'教育培训': '100200', '医疗健康': '100300',
|
|
40
49
|
};
|
|
50
|
+
const JOB_TYPE_MAP = {
|
|
51
|
+
'不限': '0', '全职': '1901', '实习': '1902', '兼职': '1903',
|
|
52
|
+
};
|
|
53
|
+
const JOB_TYPE_CODES = new Set(Object.values(JOB_TYPE_MAP));
|
|
41
54
|
function resolveCity(input) {
|
|
42
55
|
if (!input)
|
|
43
56
|
return '101010100';
|
|
@@ -62,35 +75,54 @@ function resolveMap(input, map) {
|
|
|
62
75
|
}
|
|
63
76
|
return input;
|
|
64
77
|
}
|
|
78
|
+
function resolveJobType(input) {
|
|
79
|
+
if (!input)
|
|
80
|
+
return '';
|
|
81
|
+
if (JOB_TYPE_MAP[input] !== undefined)
|
|
82
|
+
return JOB_TYPE_MAP[input];
|
|
83
|
+
if (JOB_TYPE_CODES.has(input))
|
|
84
|
+
return input;
|
|
85
|
+
throw new ArgumentError(`Invalid jobType: ${input}`, 'Use one of: 全职, 兼职, 实习, 不限');
|
|
86
|
+
}
|
|
87
|
+
function formatBossOnline(value) {
|
|
88
|
+
if (value === true)
|
|
89
|
+
return 'Y';
|
|
90
|
+
if (value === false)
|
|
91
|
+
return 'N';
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
65
94
|
cli({
|
|
66
95
|
site: 'boss',
|
|
67
96
|
name: 'search',
|
|
68
|
-
description: 'BOSS
|
|
97
|
+
description: 'BOSS直聘搜索职位(不带关键词时返回为你推荐职位)',
|
|
69
98
|
domain: 'www.zhipin.com',
|
|
70
99
|
strategy: Strategy.COOKIE,
|
|
71
100
|
navigateBefore: false,
|
|
72
101
|
browser: true,
|
|
73
102
|
args: [
|
|
74
|
-
{ name: 'query',
|
|
103
|
+
{ name: 'query', positional: true, help: 'Search keyword (optional, empty = recommended jobs)' },
|
|
75
104
|
{ name: 'city', default: '北京', help: 'City name or code (e.g. 杭州, 上海, 101010100)' },
|
|
76
|
-
{ name: 'experience', default: '', help: 'Experience:
|
|
105
|
+
{ name: 'experience', default: '', help: 'Experience: 在校生(实习)/应届生(校招)/经验不限/1年以内/1-3年/3-5年/5-10年/10年以上' },
|
|
77
106
|
{ name: 'degree', default: '', help: 'Degree: 大专/本科/硕士/博士' },
|
|
78
107
|
{ name: 'salary', default: '', help: 'Salary: 3K以下/3-5K/5-10K/10-15K/15-20K/20-30K/30-50K/50K以上' },
|
|
79
108
|
{ name: 'industry', default: '', help: 'Industry code or name (e.g. 100020, 互联网)' },
|
|
109
|
+
{ name: 'jobType', default: '', help: 'Job type: 全职/兼职/实习(不传=不限,混合校招与实习)' },
|
|
80
110
|
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
|
|
81
111
|
{ name: 'limit', type: 'int', default: 15, help: 'Number of results' },
|
|
82
112
|
],
|
|
83
|
-
columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'security_id', 'url'],
|
|
113
|
+
columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'bossOnline', 'security_id', 'url'],
|
|
84
114
|
func: async (page, kwargs) => {
|
|
85
115
|
requirePage(page);
|
|
116
|
+
const query = String(kwargs.query ?? '').trim();
|
|
86
117
|
const cityCode = resolveCity(kwargs.city);
|
|
87
118
|
verbose('Navigating to set referrer context...');
|
|
88
|
-
await navigateTo(page, `https://www.zhipin.com/web/geek/job?query=${encodeURIComponent(
|
|
119
|
+
await navigateTo(page, `https://www.zhipin.com/web/geek/job?query=${encodeURIComponent(query)}&city=${cityCode}`);
|
|
89
120
|
await new Promise(r => setTimeout(r, 1000));
|
|
90
121
|
const expVal = resolveMap(kwargs.experience, EXP_MAP);
|
|
91
122
|
const degreeVal = resolveMap(kwargs.degree, DEGREE_MAP);
|
|
92
123
|
const salaryVal = resolveMap(kwargs.salary, SALARY_MAP);
|
|
93
124
|
const industryVal = resolveMap(kwargs.industry, INDUSTRY_MAP);
|
|
125
|
+
const jobTypeVal = resolveJobType(kwargs.jobType);
|
|
94
126
|
const limit = kwargs.limit || 15;
|
|
95
127
|
let currentPage = kwargs.page || 1;
|
|
96
128
|
let allJobs = [];
|
|
@@ -101,7 +133,7 @@ cli({
|
|
|
101
133
|
}
|
|
102
134
|
const qs = new URLSearchParams({
|
|
103
135
|
scene: '1',
|
|
104
|
-
query
|
|
136
|
+
query,
|
|
105
137
|
city: cityCode,
|
|
106
138
|
page: String(currentPage),
|
|
107
139
|
pageSize: '15',
|
|
@@ -114,6 +146,8 @@ cli({
|
|
|
114
146
|
qs.set('salary', salaryVal);
|
|
115
147
|
if (industryVal)
|
|
116
148
|
qs.set('industry', industryVal);
|
|
149
|
+
if (jobTypeVal)
|
|
150
|
+
qs.set('jobType', jobTypeVal);
|
|
117
151
|
const targetUrl = `https://www.zhipin.com/wapi/zpgeek/search/joblist.json?${qs.toString()}`;
|
|
118
152
|
verbose(`Fetching page ${currentPage}... (current jobs: ${allJobs.length})`);
|
|
119
153
|
const data = await bossFetch(page, targetUrl);
|
|
@@ -135,6 +169,7 @@ cli({
|
|
|
135
169
|
degree: j.jobDegree,
|
|
136
170
|
skills: (j.skills || []).join(','),
|
|
137
171
|
boss: j.bossName + ' · ' + j.bossTitle,
|
|
172
|
+
bossOnline: formatBossOnline(j.bossOnline),
|
|
138
173
|
security_id: j.securityId || '',
|
|
139
174
|
url: 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html',
|
|
140
175
|
});
|
|
@@ -153,3 +188,9 @@ cli({
|
|
|
153
188
|
return allJobs;
|
|
154
189
|
},
|
|
155
190
|
});
|
|
191
|
+
export const __test__ = {
|
|
192
|
+
EXP_MAP,
|
|
193
|
+
resolveMap,
|
|
194
|
+
resolveJobType,
|
|
195
|
+
formatBossOnline,
|
|
196
|
+
};
|