@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/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);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerChina search — browser DOM extraction with multi-entry URL probing.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
+
import { AuthRequiredError } from '@jackwener/opencli/errors';
|
|
6
|
+
import {
|
|
7
|
+
cleanText,
|
|
8
|
+
normalizeDate,
|
|
9
|
+
toProcurementSearchRecords,
|
|
10
|
+
} from '../jianyu/shared/procurement-contract.js';
|
|
11
|
+
import { searchRowsFromEntries } from '../jianyu/shared/china-bid-search.js';
|
|
12
|
+
|
|
13
|
+
const SEARCH_ENTRIES = [
|
|
14
|
+
'https://bid.powerchina.cn/search',
|
|
15
|
+
'https://bid.powerchina.cn/',
|
|
16
|
+
];
|
|
17
|
+
const API_LIST_ENDPOINT = 'https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/list';
|
|
18
|
+
const API_DETAIL_ENDPOINT = 'https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/getInfo';
|
|
19
|
+
const API_DEFAULT_ANNOUNCEMENT_TYPE = '招采公告';
|
|
20
|
+
|
|
21
|
+
const PROCUREMENT_TITLE_HINT = /(公告|招标|采购|中标|成交|项目|notice|tender|bidding)/i;
|
|
22
|
+
const NAVIGATION_TITLE_HINT = /^(english|中文|chinese|language|home|首页|搜索|search)$/i;
|
|
23
|
+
const RETRYABLE_SEARCH_ERROR_HINT = /(detached while handling command|execution context was destroyed|target closed|cannot find context with specified id)/i;
|
|
24
|
+
|
|
25
|
+
export function buildSearchCandidates(query) {
|
|
26
|
+
const keyword = query.trim();
|
|
27
|
+
if (!keyword) return [...SEARCH_ENTRIES];
|
|
28
|
+
const encoded = encodeURIComponent(keyword);
|
|
29
|
+
return [
|
|
30
|
+
`https://bid.powerchina.cn/search?keyword=${encoded}`,
|
|
31
|
+
`https://bid.powerchina.cn/search?keywords=${encoded}`,
|
|
32
|
+
`https://bid.powerchina.cn/search?q=${encoded}`,
|
|
33
|
+
...SEARCH_ENTRIES,
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dedupeCandidates(items) {
|
|
38
|
+
const deduped = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const key = `${item.title}\t${item.url}`;
|
|
42
|
+
if (seen.has(key)) continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
deduped.push(item);
|
|
45
|
+
}
|
|
46
|
+
return deduped;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isLikelyNavigationUrl(rawUrl) {
|
|
50
|
+
const urlText = cleanText(rawUrl);
|
|
51
|
+
if (!urlText) return true;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = new URL(urlText);
|
|
54
|
+
const pathname = parsed.pathname.toLowerCase().replace(/\/+$/, '') || '/';
|
|
55
|
+
const hash = cleanText(parsed.hash).toLowerCase();
|
|
56
|
+
if (pathname === '/' || pathname === '/index') return true;
|
|
57
|
+
if (pathname === '/search') return true;
|
|
58
|
+
if (pathname === '/old' || pathname.startsWith('/old/')) return true;
|
|
59
|
+
if (pathname === '/en' || pathname.startsWith('/en/')) return true;
|
|
60
|
+
if (pathname === '/zh' || pathname.startsWith('/zh/')) return true;
|
|
61
|
+
if (hash === '#/' || hash === '#/index' || hash.startsWith('#/search')) return true;
|
|
62
|
+
return false;
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isLikelyNavigationTitle(rawTitle) {
|
|
69
|
+
const title = cleanText(rawTitle);
|
|
70
|
+
if (!title) return true;
|
|
71
|
+
const normalized = title.toLowerCase();
|
|
72
|
+
if (NAVIGATION_TITLE_HINT.test(normalized)) return true;
|
|
73
|
+
if (normalized.length <= 10 && (normalized === 'en' || normalized === 'zh' || normalized.includes('english'))) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function filterNavigationRows(items) {
|
|
80
|
+
return items.filter((item) => {
|
|
81
|
+
const title = cleanText(item.title);
|
|
82
|
+
const url = cleanText(item.url);
|
|
83
|
+
if (!url || !title) return false;
|
|
84
|
+
if (isLikelyNavigationUrl(url)) return false;
|
|
85
|
+
if (isLikelyNavigationTitle(title) && !PROCUREMENT_TITLE_HINT.test(title)) return false;
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildApiDetailUrl(id) {
|
|
91
|
+
const normalizedId = cleanText(id);
|
|
92
|
+
if (!normalizedId) return '';
|
|
93
|
+
return `${API_DETAIL_ENDPOINT}/${encodeURIComponent(normalizedId)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toApiCandidate(row) {
|
|
97
|
+
const id = cleanText(row.id);
|
|
98
|
+
const title = cleanText(row.title);
|
|
99
|
+
if (!id || !title) return null;
|
|
100
|
+
|
|
101
|
+
const url = buildApiDetailUrl(id);
|
|
102
|
+
if (!url) return null;
|
|
103
|
+
|
|
104
|
+
const contextText = cleanText([
|
|
105
|
+
row.announcementType,
|
|
106
|
+
row.titleTypeName,
|
|
107
|
+
row.source,
|
|
108
|
+
row.publishTime,
|
|
109
|
+
row.registrationDeadline,
|
|
110
|
+
row.submissionDeadline,
|
|
111
|
+
row.bidOpenTime,
|
|
112
|
+
].filter(Boolean).join(' | '));
|
|
113
|
+
|
|
114
|
+
const date = normalizeDate(cleanText(row.publishTime || row.bidOpenTime || row.submissionDeadline || ''));
|
|
115
|
+
return {
|
|
116
|
+
title,
|
|
117
|
+
url,
|
|
118
|
+
date,
|
|
119
|
+
contextText,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function searchRowsFromApi(query, limit) {
|
|
124
|
+
const keyword = cleanText(query);
|
|
125
|
+
const pageSize = Math.max(20, Math.min(100, Math.max(limit * 3, limit)));
|
|
126
|
+
const payload = {
|
|
127
|
+
pageNum: 1,
|
|
128
|
+
pageSize,
|
|
129
|
+
announcementType: API_DEFAULT_ANNOUNCEMENT_TYPE,
|
|
130
|
+
companyType: '3',
|
|
131
|
+
time: Date.now(),
|
|
132
|
+
};
|
|
133
|
+
if (keyword) payload.keyWords = keyword;
|
|
134
|
+
|
|
135
|
+
const response = await fetch(API_LIST_ENDPOINT, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json;charset=utf-8',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify(payload),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`[taxonomy=relay_unavailable] site=powerchina command=search api HTTP ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
if ((data.code ?? 200) !== 200) {
|
|
149
|
+
throw new Error(`[taxonomy=relay_unavailable] site=powerchina command=search api code=${data.code ?? 'unknown'} msg=${cleanText(data.msg)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const rows = Array.isArray(data.rows) ? data.rows : [];
|
|
153
|
+
const mapped = rows
|
|
154
|
+
.map((row) => toApiCandidate(row))
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
return dedupeCandidates(mapped).slice(0, limit);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
cli({
|
|
160
|
+
site: 'powerchina',
|
|
161
|
+
name: 'search',
|
|
162
|
+
description: '搜索中国电建阳光采购公告',
|
|
163
|
+
domain: 'bid.powerchina.cn',
|
|
164
|
+
strategy: Strategy.COOKIE,
|
|
165
|
+
browser: true,
|
|
166
|
+
args: [
|
|
167
|
+
{ name: 'query', required: true, positional: true, help: 'Search keyword, e.g. "procurement"' },
|
|
168
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
|
|
169
|
+
],
|
|
170
|
+
columns: ['rank', 'content_type', 'title', 'publish_time', 'project_code', 'budget_or_limit', 'url'],
|
|
171
|
+
func: async (page, kwargs) => {
|
|
172
|
+
const query = cleanText(kwargs.query);
|
|
173
|
+
const limit = Math.max(1, Math.min(Number(kwargs.limit) || 20, 50));
|
|
174
|
+
let extractedRows = [];
|
|
175
|
+
let apiFailure = null;
|
|
176
|
+
let apiSucceeded = false;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const apiRows = await searchRowsFromApi(query, limit);
|
|
180
|
+
extractedRows = apiRows;
|
|
181
|
+
apiSucceeded = true;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
apiFailure = cleanText(error instanceof Error ? error.message : String(error || ''));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (apiSucceeded && extractedRows.length === 0) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!apiSucceeded) {
|
|
191
|
+
try {
|
|
192
|
+
extractedRows = await searchRowsFromEntries(page, {
|
|
193
|
+
query,
|
|
194
|
+
candidateUrls: buildSearchCandidates(query),
|
|
195
|
+
allowedHostFragments: ['bid.powerchina.cn', 'powerchina.cn'],
|
|
196
|
+
limit,
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const message = cleanText(error instanceof Error ? error.message : String(error || ''));
|
|
200
|
+
if (RETRYABLE_SEARCH_ERROR_HINT.test(message)) {
|
|
201
|
+
throw new Error(`[taxonomy=relay_unavailable] site=powerchina command=search detached browser context: ${message}`);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rows = filterNavigationRows(
|
|
208
|
+
dedupeCandidates(extractedRows).map((item) => ({
|
|
209
|
+
title: cleanText(item.title),
|
|
210
|
+
url: cleanText(item.url),
|
|
211
|
+
date: normalizeDate(cleanText(item.date)),
|
|
212
|
+
contextText: cleanText(item.contextText),
|
|
213
|
+
})),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (rows.length === 0 && extractedRows.length > 0) {
|
|
217
|
+
throw new Error('[taxonomy=empty_result] site=powerchina command=search extracted only navigation/portal rows');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (rows.length === 0) {
|
|
221
|
+
const pageText = cleanText(await page.evaluate('document.body ? document.body.innerText : ""'));
|
|
222
|
+
if (/(请先登录|未登录|登录后|验证码|人机验证)/.test(pageText)) {
|
|
223
|
+
throw new AuthRequiredError(
|
|
224
|
+
'bid.powerchina.cn',
|
|
225
|
+
'[taxonomy=selector_drift] site=powerchina command=search login required or human verification',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (apiFailure) {
|
|
229
|
+
throw new Error(`[taxonomy=empty_result] site=powerchina command=search api/dom yielded no result: ${apiFailure}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return toProcurementSearchRecords(rows, {
|
|
234
|
+
site: 'powerchina',
|
|
235
|
+
query,
|
|
236
|
+
limit,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
export const __test__ = {
|
|
242
|
+
buildSearchCandidates,
|
|
243
|
+
normalizeDate,
|
|
244
|
+
dedupeCandidates,
|
|
245
|
+
filterNavigationRows,
|
|
246
|
+
isLikelyNavigationUrl,
|
|
247
|
+
isLikelyNavigationTitle,
|
|
248
|
+
buildApiDetailUrl,
|
|
249
|
+
toApiCandidate,
|
|
250
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './search.js';
|
|
3
|
+
|
|
4
|
+
describe('powerchina search helpers', () => {
|
|
5
|
+
it('builds candidate URLs with keyword variants', () => {
|
|
6
|
+
const candidates = __test__.buildSearchCandidates('procurement');
|
|
7
|
+
expect(candidates[0]).toContain('keyword=procurement');
|
|
8
|
+
expect(candidates.some((item) => item.includes('/search?keywords='))).toBe(true);
|
|
9
|
+
expect(candidates.some((item) => item === 'https://bid.powerchina.cn/search')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('normalizes date text', () => {
|
|
13
|
+
expect(__test__.normalizeDate('2026-4-7')).toBe('2026-04-07');
|
|
14
|
+
expect(__test__.normalizeDate('公告时间:2026年04月07日')).toBe('2026-04-07');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('deduplicates title/url pairs', () => {
|
|
18
|
+
const deduped = __test__.dedupeCandidates([
|
|
19
|
+
{ title: 'A', url: 'https://a.com/1', date: '2026-04-07' },
|
|
20
|
+
{ title: 'A', url: 'https://a.com/1', date: '2026-04-07' },
|
|
21
|
+
{ title: 'B', url: 'https://a.com/1', date: '2026-04-07' },
|
|
22
|
+
]);
|
|
23
|
+
expect(deduped).toHaveLength(2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('filters obvious navigation rows before quality gate', () => {
|
|
27
|
+
const filtered = __test__.filterNavigationRows([
|
|
28
|
+
{ title: '搜索', url: 'https://bid.powerchina.cn/search', date: '2026-04-07' },
|
|
29
|
+
{ title: '首页', url: 'https://bid.powerchina.cn/', date: '2026-04-07' },
|
|
30
|
+
{ title: 'English', url: 'https://bid.powerchina.cn/old/en', date: '' },
|
|
31
|
+
{ title: '某项目电梯采购公告', url: 'https://bid.powerchina.cn/notice/detail?id=123', date: '2026-04-07' },
|
|
32
|
+
]);
|
|
33
|
+
expect(filtered).toHaveLength(1);
|
|
34
|
+
expect(filtered[0].title).toContain('电梯采购公告');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('treats old/en language switch urls as navigation', () => {
|
|
38
|
+
expect(__test__.isLikelyNavigationUrl('https://bid.powerchina.cn/old/en')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('treats language-toggle labels as navigation titles', () => {
|
|
42
|
+
expect(__test__.isLikelyNavigationTitle('English')).toBe(true);
|
|
43
|
+
expect(__test__.isLikelyNavigationTitle('EN')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('builds api detail urls with stable id', () => {
|
|
47
|
+
const url = __test__.buildApiDetailUrl('2409419657');
|
|
48
|
+
expect(url).toBe('https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/getInfo/2409419657');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('maps api rows into normalized search candidates', () => {
|
|
52
|
+
const mapped = __test__.toApiCandidate({
|
|
53
|
+
id: '2409419657',
|
|
54
|
+
title: '某项目电梯采购公告',
|
|
55
|
+
announcementType: '招采公告',
|
|
56
|
+
companyType: '3',
|
|
57
|
+
titleTypeName: '货物类',
|
|
58
|
+
source: '设备物资集中采购电子平台',
|
|
59
|
+
publishTime: '2026-04-07 17:05:02',
|
|
60
|
+
submissionDeadline: '2026-04-14',
|
|
61
|
+
});
|
|
62
|
+
expect(mapped).not.toBeNull();
|
|
63
|
+
expect(mapped?.title).toContain('电梯采购公告');
|
|
64
|
+
expect(mapped?.date).toBe('2026-04-07');
|
|
65
|
+
expect(mapped?.url).toBe('https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/getInfo/2409419657');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -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 = {
|
|
@@ -79,12 +79,15 @@ cli({
|
|
|
79
79
|
if (!entries.length) {
|
|
80
80
|
throw new CliError('NOT_FOUND', `No stock found for "${key}"`, 'Try a different name, code, or --market');
|
|
81
81
|
}
|
|
82
|
-
// Pick best match: score by name similarity, tiebreak by market priority
|
|
82
|
+
// Pick best match: score by name/symbol similarity, tiebreak by market priority
|
|
83
83
|
const needle = key.toLowerCase();
|
|
84
84
|
const score = (e) => {
|
|
85
85
|
const n = e.name.toLowerCase();
|
|
86
|
-
|
|
86
|
+
const s = e.symbol.toLowerCase();
|
|
87
|
+
if (s === needle || n === needle)
|
|
87
88
|
return 1;
|
|
89
|
+
if (s.includes(needle))
|
|
90
|
+
return needle.length / s.length;
|
|
88
91
|
if (n.includes(needle))
|
|
89
92
|
return needle.length / n.length;
|
|
90
93
|
return 0;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './stock.js';
|
|
4
|
+
|
|
5
|
+
function textResponse(body) {
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
arrayBuffer: async () => Buffer.from(body, 'utf8'),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('sinafinance stock command', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
vi.stubGlobal('TextDecoder', class {
|
|
16
|
+
decode(buf) {
|
|
17
|
+
return Buffer.from(buf).toString('utf8');
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('prefers exact symbol match over partial symbol and name misses', async () => {
|
|
23
|
+
const cmd = getRegistry().get('sinafinance/stock');
|
|
24
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
25
|
+
|
|
26
|
+
const fetchMock = vi.fn()
|
|
27
|
+
.mockResolvedValueOnce(textResponse('var suggestvalue="x,41,,AAPL,苹果;x,41,,AAPLU,Apple Units";'))
|
|
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
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
30
|
+
|
|
31
|
+
const result = await cmd.func({ key: 'AAPL', market: 'auto' });
|
|
32
|
+
|
|
33
|
+
expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://suggest3.sinajs.cn/suggest/type=11,31,41&key=AAPL', expect.any(Object));
|
|
34
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://hq.sinajs.cn/list=gb_AAPL', expect.any(Object));
|
|
35
|
+
expect(result[0]).toMatchObject({
|
|
36
|
+
Symbol: 'AAPL',
|
|
37
|
+
Name: 'Apple Inc',
|
|
38
|
+
Price: '189.98',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('still matches by display name when the query targets the company name', async () => {
|
|
43
|
+
const cmd = getRegistry().get('sinafinance/stock');
|
|
44
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
45
|
+
|
|
46
|
+
const fetchMock = vi.fn()
|
|
47
|
+
.mockResolvedValueOnce(textResponse('var suggestvalue="x,41,,AAPL,苹果;x,41,,AAPLU,Apple Units";'))
|
|
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
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
50
|
+
|
|
51
|
+
const result = await cmd.func({ key: '苹果', market: 'auto' });
|
|
52
|
+
|
|
53
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://hq.sinajs.cn/list=gb_AAPL', expect.any(Object));
|
|
54
|
+
expect(result[0]).toMatchObject({
|
|
55
|
+
Symbol: 'AAPL',
|
|
56
|
+
Name: '苹果公司',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
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)
|