@jackwener/opencli 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/README.zh-CN.md +6 -3
- package/cli-manifest.json +1085 -73
- package/clis/barchart/flow.js +1 -1
- package/clis/barchart/greeks.js +2 -2
- package/clis/barchart/options.js +2 -2
- package/clis/barchart/quote.js +1 -1
- package/clis/bilibili/feed.js +202 -48
- package/clis/binance/asks.js +21 -0
- package/clis/binance/commands.test.js +70 -0
- package/clis/binance/depth.js +21 -0
- package/clis/binance/gainers.js +22 -0
- package/clis/binance/klines.js +21 -0
- package/clis/binance/losers.js +22 -0
- package/clis/binance/pairs.js +21 -0
- package/clis/binance/price.js +18 -0
- package/clis/binance/prices.js +19 -0
- package/clis/binance/ticker.js +21 -0
- package/clis/binance/top.js +21 -0
- package/clis/binance/trades.js +20 -0
- package/clis/boss/utils.js +2 -1
- package/clis/chatgpt/image.js +97 -0
- package/clis/chatgpt/utils.js +297 -0
- package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
- package/clis/discord-app/delete.js +114 -0
- package/clis/douban/utils.js +29 -2
- package/clis/douban/utils.test.js +121 -1
- package/clis/ke/chengjiao.js +77 -0
- package/clis/ke/ershoufang.js +100 -0
- package/clis/ke/utils.js +104 -0
- package/clis/ke/xiaoqu.js +77 -0
- package/clis/ke/zufang.js +94 -0
- package/clis/maimai/search-talents.js +172 -0
- package/clis/mubu/doc.js +40 -0
- package/clis/mubu/docs.js +43 -0
- package/clis/mubu/notes.js +244 -0
- package/clis/mubu/recent.js +27 -0
- package/clis/mubu/search.js +62 -0
- package/clis/mubu/utils.js +304 -0
- package/clis/reuters/search.js +1 -1
- package/clis/twitter/lists-parser.js +77 -0
- package/clis/twitter/lists.d.ts +5 -0
- package/clis/twitter/lists.js +62 -0
- package/clis/twitter/lists.test.js +50 -0
- package/clis/weibo/feed.js +18 -5
- package/clis/xiaohongshu/comments.js +18 -6
- package/clis/xiaohongshu/comments.test.js +36 -0
- package/clis/xiaohongshu/creator-note-detail.js +2 -0
- package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
- package/clis/xiaohongshu/creator-notes-summary.js +4 -0
- package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
- package/clis/xiaohongshu/creator-notes.js +1 -0
- package/clis/xiaohongshu/creator-profile.js +1 -0
- package/clis/xiaohongshu/creator-stats.js +1 -0
- package/clis/xiaohongshu/download.js +12 -0
- package/clis/xiaohongshu/download.test.js +30 -0
- package/clis/xiaohongshu/navigation.test.js +34 -0
- package/clis/xiaohongshu/note.js +14 -5
- package/clis/xiaohongshu/note.test.js +28 -0
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/yahoo-finance/quote.js +1 -1
- package/clis/zsxq/topic.js +5 -3
- package/clis/zsxq/topic.test.js +4 -3
- package/clis/zsxq/utils.js +1 -1
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +19 -0
- package/dist/src/browser/cdp.js +10 -2
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/cli.js +112 -2
- package/dist/src/daemon.js +5 -0
- package/dist/src/discovery.d.ts +5 -2
- package/dist/src/discovery.js +7 -35
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +51 -2
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/engine.test.js +29 -1
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.js +13 -0
- package/dist/src/execution.js +36 -9
- package/dist/src/execution.test.js +23 -0
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +4 -9
- package/dist/src/main.js +6 -5
- package/dist/src/registry.js +3 -4
- package/dist/src/types.d.ts +2 -0
- package/dist/src/update-check.d.ts +14 -0
- package/dist/src/update-check.js +48 -3
- package/dist/src/update-check.test.js +31 -0
- package/package.json +3 -3
- package/scripts/fetch-adapters.js +92 -34
- package/dist/src/clis/binance/asks.js +0 -20
- package/dist/src/clis/binance/commands.test.d.ts +0 -3
- package/dist/src/clis/binance/commands.test.js +0 -58
- package/dist/src/clis/binance/depth.d.ts +0 -1
- package/dist/src/clis/binance/depth.js +0 -20
- package/dist/src/clis/binance/gainers.d.ts +0 -1
- package/dist/src/clis/binance/gainers.js +0 -21
- package/dist/src/clis/binance/klines.d.ts +0 -1
- package/dist/src/clis/binance/klines.js +0 -20
- package/dist/src/clis/binance/losers.d.ts +0 -1
- package/dist/src/clis/binance/losers.js +0 -21
- package/dist/src/clis/binance/pairs.d.ts +0 -1
- package/dist/src/clis/binance/pairs.js +0 -20
- package/dist/src/clis/binance/price.d.ts +0 -1
- package/dist/src/clis/binance/price.js +0 -17
- package/dist/src/clis/binance/prices.d.ts +0 -1
- package/dist/src/clis/binance/prices.js +0 -18
- package/dist/src/clis/binance/ticker.d.ts +0 -1
- package/dist/src/clis/binance/ticker.js +0 -20
- package/dist/src/clis/binance/top.d.ts +0 -1
- package/dist/src/clis/binance/top.js +0 -20
- package/dist/src/clis/binance/trades.d.ts +0 -1
- package/dist/src/clis/binance/trades.js +0 -19
- /package/clis/{chatgpt → chatgpt-app}/ax.js +0 -0
- /package/dist/src/{clis/binance/asks.d.ts → update-check.test.d.ts} +0 -0
package/clis/barchart/flow.js
CHANGED
|
@@ -26,7 +26,7 @@ cli({
|
|
|
26
26
|
const data = await page.evaluate(`
|
|
27
27
|
(async () => {
|
|
28
28
|
const limit = ${limit};
|
|
29
|
-
const typeFilter =
|
|
29
|
+
const typeFilter = ${JSON.stringify(optionType)}.toLowerCase();
|
|
30
30
|
|
|
31
31
|
// Wait for CSRF token to appear (Angular may inject it after initial render)
|
|
32
32
|
let csrf = '';
|
package/clis/barchart/greeks.js
CHANGED
|
@@ -27,8 +27,8 @@ cli({
|
|
|
27
27
|
await page.wait(4);
|
|
28
28
|
const data = await page.evaluate(`
|
|
29
29
|
(async () => {
|
|
30
|
-
const sym =
|
|
31
|
-
const expDate =
|
|
30
|
+
const sym = ${JSON.stringify(symbol)};
|
|
31
|
+
const expDate = ${JSON.stringify(expiration)};
|
|
32
32
|
const limit = ${limit};
|
|
33
33
|
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
34
34
|
const headers = { 'X-CSRF-TOKEN': csrf };
|
package/clis/barchart/options.js
CHANGED
|
@@ -26,8 +26,8 @@ cli({
|
|
|
26
26
|
await page.wait(4);
|
|
27
27
|
const data = await page.evaluate(`
|
|
28
28
|
(async () => {
|
|
29
|
-
const sym =
|
|
30
|
-
const type =
|
|
29
|
+
const sym = ${JSON.stringify(symbol)};
|
|
30
|
+
const type = ${JSON.stringify(optType)};
|
|
31
31
|
const limit = ${limit};
|
|
32
32
|
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
33
33
|
const headers = { 'X-CSRF-TOKEN': csrf };
|
package/clis/barchart/quote.js
CHANGED
|
@@ -24,7 +24,7 @@ cli({
|
|
|
24
24
|
await page.wait(4);
|
|
25
25
|
const data = await page.evaluate(`
|
|
26
26
|
(async () => {
|
|
27
|
-
const sym =
|
|
27
|
+
const sym = ${JSON.stringify(symbol)};
|
|
28
28
|
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
29
29
|
|
|
30
30
|
// Strategy 1: internal proxy API with CSRF token
|
package/clis/bilibili/feed.js
CHANGED
|
@@ -1,64 +1,218 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import { apiGet, payloadData, stripHtml } from './utils.js';
|
|
2
|
+
import { apiGet, payloadData, resolveUid, stripHtml } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/** Map bilibili dynamic type to readable short name */
|
|
5
|
+
const TYPE_MAP = {
|
|
6
|
+
DYNAMIC_TYPE_AV: 'video',
|
|
7
|
+
DYNAMIC_TYPE_DRAW: 'draw',
|
|
8
|
+
DYNAMIC_TYPE_ARTICLE: 'article',
|
|
9
|
+
DYNAMIC_TYPE_FORWARD: 'forward',
|
|
10
|
+
DYNAMIC_TYPE_WORD: 'text',
|
|
11
|
+
DYNAMIC_TYPE_LIVE_RCMD: 'live',
|
|
12
|
+
DYNAMIC_TYPE_PGC: 'bangumi',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function parseItem(item) {
|
|
16
|
+
const modules = item.modules ?? {};
|
|
17
|
+
const authorModule = modules.module_author ?? {};
|
|
18
|
+
const dynamicModule = modules.module_dynamic ?? {};
|
|
19
|
+
const major = dynamicModule.major ?? {};
|
|
20
|
+
const stat = modules.module_stat ?? {};
|
|
21
|
+
|
|
22
|
+
let title = '';
|
|
23
|
+
let url = item.id_str ? `https://t.bilibili.com/${item.id_str}` : '';
|
|
24
|
+
const itemType = TYPE_MAP[item.type] ?? item.type ?? '';
|
|
25
|
+
|
|
26
|
+
// video
|
|
27
|
+
if (major.archive) {
|
|
28
|
+
title = major.archive.title ?? '';
|
|
29
|
+
url = major.archive.jump_url ? `https:${major.archive.jump_url}` : url;
|
|
30
|
+
}
|
|
31
|
+
// article
|
|
32
|
+
if (!title && major.article) {
|
|
33
|
+
title = major.article.title ?? '';
|
|
34
|
+
url = major.article.jump_url ? `https:${major.article.jump_url}` : url;
|
|
35
|
+
}
|
|
36
|
+
// text content in desc
|
|
37
|
+
if (!title && dynamicModule.desc?.text) {
|
|
38
|
+
title = stripHtml(dynamicModule.desc.text).slice(0, 60);
|
|
39
|
+
}
|
|
40
|
+
// draw (图文) — use opus or draw items count as hint
|
|
41
|
+
if (!title && major.draw) {
|
|
42
|
+
const imgCount = major.draw.items?.length ?? 0;
|
|
43
|
+
title = imgCount > 0 ? `[图片x${imgCount}]` : '[图文动态]';
|
|
44
|
+
}
|
|
45
|
+
// VIP only content
|
|
46
|
+
if (!title && item.basic?.is_only_fans) {
|
|
47
|
+
title = '[充电专属]';
|
|
48
|
+
}
|
|
49
|
+
// forward
|
|
50
|
+
if (!title && item.type === 'DYNAMIC_TYPE_FORWARD') {
|
|
51
|
+
title = '[转发动态]';
|
|
52
|
+
}
|
|
53
|
+
// final fallback
|
|
54
|
+
if (!title) {
|
|
55
|
+
title = `[${itemType || '动态'}]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const time = authorModule.pub_time ?? '';
|
|
59
|
+
const likes = stat.like?.count ?? 0;
|
|
60
|
+
const comments = stat.comment?.count ?? 0;
|
|
61
|
+
|
|
62
|
+
return { title, url, itemType, author: authorModule.name ?? '', time, likes, comments };
|
|
63
|
+
}
|
|
64
|
+
|
|
3
65
|
cli({
|
|
4
66
|
site: 'bilibili',
|
|
5
67
|
name: 'feed',
|
|
6
|
-
description: '
|
|
68
|
+
description: '动态时间线(不传 uid 查关注时间线,传 uid 查指定用户动态)',
|
|
7
69
|
domain: 'www.bilibili.com',
|
|
8
70
|
strategy: Strategy.COOKIE,
|
|
9
71
|
args: [
|
|
10
|
-
{ name: '
|
|
11
|
-
{ name: '
|
|
72
|
+
{ name: 'uid', positional: true, required: false, help: '用户 UID 或用户名(不传则显示关注时间线)' },
|
|
73
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max results to return' },
|
|
74
|
+
{ name: 'type', default: 'all', help: 'Filter: all, video, article, draw, text' },
|
|
75
|
+
{ name: 'pages', type: 'int', default: 1, help: 'Number of pages to fetch (each ~20 items)' },
|
|
12
76
|
],
|
|
13
|
-
columns: ['rank', 'author', 'title', 'type', 'url'],
|
|
77
|
+
columns: ['rank', 'time', 'author', 'title', 'type', 'likes', 'url'],
|
|
14
78
|
func: async (page, kwargs) => {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
page: 1,
|
|
23
|
-
...(updateBaseline ? { update_baseline: updateBaseline } : {}),
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
const items = payloadData(payload)?.items ?? [];
|
|
79
|
+
const maxResults = Number(kwargs.limit) || 20;
|
|
80
|
+
const maxPages = Number(kwargs.pages) || 1;
|
|
81
|
+
const filterType = kwargs.type === 'all' ? '' : (kwargs.type ?? '');
|
|
82
|
+
|
|
83
|
+
const isUserFeed = !!kwargs.uid;
|
|
84
|
+
const uid = isUserFeed ? await resolveUid(page, String(kwargs.uid)) : null;
|
|
85
|
+
|
|
27
86
|
const rows = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
87
|
+
let offset = '';
|
|
88
|
+
|
|
89
|
+
for (let p = 0; p < maxPages; p++) {
|
|
90
|
+
if (rows.length >= maxResults) break;
|
|
91
|
+
|
|
92
|
+
let payload;
|
|
93
|
+
if (isUserFeed) {
|
|
94
|
+
const params = { host_mid: uid, timezone_offset: -480 };
|
|
95
|
+
if (offset) params.offset = offset;
|
|
96
|
+
payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/space', { params });
|
|
97
|
+
} else {
|
|
98
|
+
const params = {
|
|
99
|
+
timezone_offset: -480,
|
|
100
|
+
type: filterType || 'all',
|
|
101
|
+
page: p + 1,
|
|
102
|
+
};
|
|
103
|
+
if (offset) params.offset = offset;
|
|
104
|
+
payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/all', { params });
|
|
46
105
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
106
|
+
|
|
107
|
+
const data = payloadData(payload) ?? {};
|
|
108
|
+
const items = data.items ?? [];
|
|
109
|
+
if (items.length === 0) break;
|
|
110
|
+
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
if (rows.length >= maxResults) break;
|
|
113
|
+
const parsed = parseItem(item);
|
|
114
|
+
if (filterType && parsed.itemType !== filterType) continue;
|
|
115
|
+
rows.push({
|
|
116
|
+
rank: rows.length + 1,
|
|
117
|
+
time: parsed.time,
|
|
118
|
+
author: parsed.author,
|
|
119
|
+
title: parsed.title,
|
|
120
|
+
type: parsed.itemType,
|
|
121
|
+
likes: parsed.likes,
|
|
122
|
+
url: parsed.url,
|
|
123
|
+
});
|
|
51
124
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
125
|
+
|
|
126
|
+
offset = data.offset ?? items[items.length - 1]?.id_str ?? '';
|
|
127
|
+
if (!offset || !data.has_more) break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return rows;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
cli({
|
|
135
|
+
site: 'bilibili',
|
|
136
|
+
name: 'feed-detail',
|
|
137
|
+
description: '查看 Bilibili 动态详情(支持充电专属内容)',
|
|
138
|
+
domain: 'www.bilibili.com',
|
|
139
|
+
strategy: Strategy.COOKIE,
|
|
140
|
+
args: [
|
|
141
|
+
{ name: 'id', positional: true, required: true, help: '动态 ID(从 feed 命令的 url 中获取)' },
|
|
142
|
+
],
|
|
143
|
+
columns: ['field', 'value'],
|
|
144
|
+
func: async (page, kwargs) => {
|
|
145
|
+
const id = String(kwargs.id);
|
|
146
|
+
const payload = await apiGet(page, '/x/polymer/web-dynamic/v1/detail', {
|
|
147
|
+
params: { id, timezone_offset: -480 },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const rows = [];
|
|
151
|
+
const data = payloadData(payload);
|
|
152
|
+
const item = data?.item;
|
|
153
|
+
if (!item) {
|
|
154
|
+
rows.push({ field: 'error', value: '动态不存在或无权查看'});
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const modules = item.modules ?? {};
|
|
159
|
+
const author = modules.module_author ?? {};
|
|
160
|
+
const dynamicModule = modules.module_dynamic ?? {};
|
|
161
|
+
const major = dynamicModule.major ?? {};
|
|
162
|
+
const stat = modules.module_stat ?? {};
|
|
163
|
+
|
|
164
|
+
rows.push({ field: 'id', value: item.id_str ?? id });
|
|
165
|
+
rows.push({ field: 'author', value: author.name ?? '' });
|
|
166
|
+
rows.push({ field: 'time', value: author.pub_time ?? '' });
|
|
167
|
+
rows.push({ field: 'type', value: TYPE_MAP[item.type] ?? item.type ?? '' });
|
|
168
|
+
|
|
169
|
+
// text content
|
|
170
|
+
if (dynamicModule.desc?.text) {
|
|
171
|
+
rows.push({ field: 'text', value: stripHtml(dynamicModule.desc.text) });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// video
|
|
175
|
+
if (major.archive) {
|
|
176
|
+
rows.push({ field: 'video_title', value: major.archive.title ?? '' });
|
|
177
|
+
rows.push({ field: 'video_desc', value: major.archive.desc ?? '' });
|
|
178
|
+
rows.push({ field: 'video_url', value: major.archive.jump_url ? `https:${major.archive.jump_url}` : '' });
|
|
179
|
+
rows.push({ field: 'play', value: String(major.archive.stat?.play ?? '') });
|
|
180
|
+
rows.push({ field: 'danmaku', value: String(major.archive.stat?.danmaku ?? '') });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// article
|
|
184
|
+
if (major.article) {
|
|
185
|
+
rows.push({ field: 'article_title', value: major.article.title ?? '' });
|
|
186
|
+
rows.push({ field: 'article_url', value: major.article.jump_url ? `https:${major.article.jump_url}` : '' });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// draw (images)
|
|
190
|
+
if (major.draw?.items?.length) {
|
|
191
|
+
rows.push({ field: 'images', value: major.draw.items.map((img) => img.src).join('\n') });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// opus (rich text, some dynamics use this)
|
|
195
|
+
if (major.opus?.summary?.text) {
|
|
196
|
+
rows.push({ field: 'opus_text', value: stripHtml(major.opus.summary.text) });
|
|
197
|
+
}
|
|
198
|
+
if (major.opus?.title) {
|
|
199
|
+
rows.push({ field: 'opus_title', value: major.opus.title });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// forward - show original dynamic info
|
|
203
|
+
if (item.orig) {
|
|
204
|
+
const origAuthor = item.orig.modules?.module_author?.name ?? '';
|
|
205
|
+
const origDesc = item.orig.modules?.module_dynamic?.desc?.text ?? '';
|
|
206
|
+
rows.push({ field: 'forward_from', value: origAuthor });
|
|
207
|
+
if (origDesc) rows.push({ field: 'forward_text', value: stripHtml(origDesc).slice(0, 200) });
|
|
61
208
|
}
|
|
209
|
+
|
|
210
|
+
// stats
|
|
211
|
+
rows.push({ field: 'likes', value: String(stat.like?.count ?? 0) });
|
|
212
|
+
rows.push({ field: 'comments', value: String(stat.comment?.count ?? 0) });
|
|
213
|
+
rows.push({ field: 'forwards', value: String(stat.forward?.count ?? 0) });
|
|
214
|
+
rows.push({ field: 'url', value: `https://t.bilibili.com/${item.id_str ?? id}` });
|
|
215
|
+
|
|
62
216
|
return rows;
|
|
63
217
|
},
|
|
64
218
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'asks',
|
|
6
|
+
description: 'Order book ask prices for a trading pair',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['rank', 'ask_price', 'ask_qty'],
|
|
15
|
+
pipeline: [
|
|
16
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
|
|
17
|
+
{ select: 'asks' },
|
|
18
|
+
{ map: { rank: '${{ index + 1 }}', ask_price: '${{ item.0 }}', ask_qty: '${{ item.1 }}' } },
|
|
19
|
+
{ limit: '${{ args.limit }}' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { executePipeline } from '@jackwener/opencli/pipeline';
|
|
4
|
+
|
|
5
|
+
// Import all binance adapters to register them
|
|
6
|
+
import './top.js';
|
|
7
|
+
import './gainers.js';
|
|
8
|
+
import './pairs.js';
|
|
9
|
+
|
|
10
|
+
function loadPipeline(name) {
|
|
11
|
+
const cmd = getRegistry().get(`binance/${name}`);
|
|
12
|
+
if (!cmd?.pipeline) throw new Error(`Command binance/${name} not found or has no pipeline`);
|
|
13
|
+
return cmd.pipeline;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mockJsonOnce(payload) {
|
|
17
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
18
|
+
ok: true,
|
|
19
|
+
status: 200,
|
|
20
|
+
statusText: 'OK',
|
|
21
|
+
json: vi.fn().mockResolvedValue(payload),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.unstubAllGlobals();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('binance adapters', () => {
|
|
31
|
+
it('sorts top pairs by numeric quote volume', async () => {
|
|
32
|
+
mockJsonOnce([
|
|
33
|
+
{ symbol: 'SMALL', lastPrice: '1', priceChangePercent: '1.2', highPrice: '1', lowPrice: '1', quoteVolume: '9.9' },
|
|
34
|
+
{ symbol: 'LARGE', lastPrice: '2', priceChangePercent: '2.3', highPrice: '2', lowPrice: '2', quoteVolume: '100.0' },
|
|
35
|
+
{ symbol: 'MID', lastPrice: '3', priceChangePercent: '3.4', highPrice: '3', lowPrice: '3', quoteVolume: '11.0' },
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const result = await executePipeline(null, loadPipeline('top'), { args: { limit: 3 } });
|
|
39
|
+
|
|
40
|
+
expect(result.map((item) => item.symbol)).toEqual(['LARGE', 'MID', 'SMALL']);
|
|
41
|
+
expect(result.map((item) => item.rank)).toEqual([1, 2, 3]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('sorts gainers by numeric percent change', async () => {
|
|
45
|
+
mockJsonOnce([
|
|
46
|
+
{ symbol: 'TEN', lastPrice: '1', priceChangePercent: '10.0', quoteVolume: '100' },
|
|
47
|
+
{ symbol: 'NINE', lastPrice: '1', priceChangePercent: '9.5', quoteVolume: '100' },
|
|
48
|
+
{ symbol: 'HUNDRED', lastPrice: '1', priceChangePercent: '100.0', quoteVolume: '100' },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const result = await executePipeline(null, loadPipeline('gainers'), { args: { limit: 3 } });
|
|
52
|
+
|
|
53
|
+
expect(result.map((item) => item.symbol)).toEqual(['HUNDRED', 'TEN', 'NINE']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('keeps only TRADING pairs', async () => {
|
|
57
|
+
mockJsonOnce({
|
|
58
|
+
symbols: [
|
|
59
|
+
{ symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', status: 'TRADING' },
|
|
60
|
+
{ symbol: 'OLDPAIR', baseAsset: 'OLD', quoteAsset: 'USDT', status: 'BREAK' },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = await executePipeline(null, loadPipeline('pairs'), { args: { limit: 10 } });
|
|
65
|
+
|
|
66
|
+
expect(result).toEqual([
|
|
67
|
+
{ symbol: 'BTCUSDT', base: 'BTC', quote: 'USDT', status: 'TRADING' },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'depth',
|
|
6
|
+
description: 'Order book bid prices for a trading pair',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['rank', 'bid_price', 'bid_qty'],
|
|
15
|
+
pipeline: [
|
|
16
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
|
|
17
|
+
{ select: 'bids' },
|
|
18
|
+
{ map: { rank: '${{ index + 1 }}', bid_price: '${{ item.0 }}', bid_qty: '${{ item.1 }}' } },
|
|
19
|
+
{ limit: '${{ args.limit }}' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'gainers',
|
|
6
|
+
description: 'Top gaining trading pairs by 24h price change',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of trading pairs' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'symbol', 'price', 'change_24h', 'volume'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
|
|
16
|
+
{ filter: 'item.priceChangePercent' },
|
|
17
|
+
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}', sort_change: '${{ Number(item.priceChangePercent) }}' } },
|
|
18
|
+
{ sort: { by: 'sort_change', order: 'desc' } },
|
|
19
|
+
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}' } },
|
|
20
|
+
{ limit: '${{ args.limit }}' },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'klines',
|
|
6
|
+
description: 'Candlestick/kline data for a trading pair',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
12
|
+
{ name: 'interval', type: 'str', default: '1d', help: 'Kline interval (1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M)' },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of klines (max 1000)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['open', 'high', 'low', 'close', 'volume'],
|
|
16
|
+
pipeline: [
|
|
17
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/klines?symbol=${{ args.symbol }}&interval=${{ args.interval }}&limit=${{ args.limit }}' } },
|
|
18
|
+
{ map: { open: '${{ item.1 }}', high: '${{ item.2 }}', low: '${{ item.3 }}', close: '${{ item.4 }}', volume: '${{ item.5 }}' } },
|
|
19
|
+
{ limit: '${{ args.limit }}' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'losers',
|
|
6
|
+
description: 'Top losing trading pairs by 24h price change',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of trading pairs' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'symbol', 'price', 'change_24h', 'volume'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
|
|
16
|
+
{ filter: 'item.priceChangePercent' },
|
|
17
|
+
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}', sort_change: '${{ Number(item.priceChangePercent) }}' } },
|
|
18
|
+
{ sort: { by: 'sort_change' } },
|
|
19
|
+
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}' } },
|
|
20
|
+
{ limit: '${{ args.limit }}' },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'pairs',
|
|
6
|
+
description: 'List active trading pairs on Binance',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of trading pairs' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['symbol', 'base', 'quote', 'status'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/exchangeInfo' } },
|
|
16
|
+
{ select: 'symbols' },
|
|
17
|
+
{ filter: 'item.status === \'TRADING\'' },
|
|
18
|
+
{ map: { symbol: '${{ item.symbol }}', base: '${{ item.baseAsset }}', quote: '${{ item.quoteAsset }}', status: '${{ item.status }}' } },
|
|
19
|
+
{ limit: '${{ args.limit }}' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'price',
|
|
6
|
+
description: 'Quick price check for a trading pair',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['symbol', 'price', 'change', 'change_pct', 'high', 'low', 'volume', 'quote_volume', 'trades'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr?symbol=${{ args.symbol }}' } },
|
|
16
|
+
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change: '${{ item.priceChange }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_volume: '${{ item.quoteVolume }}', trades: '${{ item.count }}' } },
|
|
17
|
+
],
|
|
18
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'prices',
|
|
6
|
+
description: 'Latest prices for all trading pairs',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of prices' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'symbol', 'price'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/price' } },
|
|
16
|
+
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.price }}' } },
|
|
17
|
+
{ limit: '${{ args.limit }}' },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'ticker',
|
|
6
|
+
description: '24h ticker statistics for top trading pairs by volume',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of tickers' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['symbol', 'price', 'change_pct', 'high', 'low', 'volume', 'quote_vol', 'trades'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
|
|
16
|
+
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_vol: '${{ item.quoteVolume }}', trades: '${{ item.count }}', sort_volume: '${{ Number(item.quoteVolume) }}' } },
|
|
17
|
+
{ sort: { by: 'sort_volume', order: 'desc' } },
|
|
18
|
+
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_vol: '${{ item.quoteVolume }}', trades: '${{ item.count }}' } },
|
|
19
|
+
{ limit: '${{ args.limit }}' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'top',
|
|
6
|
+
description: 'Top trading pairs by 24h volume on Binance',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of trading pairs' },
|
|
12
|
+
],
|
|
13
|
+
columns: ['rank', 'symbol', 'price', 'change_24h', 'high', 'low', 'volume'],
|
|
14
|
+
pipeline: [
|
|
15
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
|
|
16
|
+
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.quoteVolume }}', sort_volume: '${{ Number(item.quoteVolume) }}' } },
|
|
17
|
+
{ sort: { by: 'sort_volume', order: 'desc' } },
|
|
18
|
+
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.quoteVolume }}' } },
|
|
19
|
+
{ limit: '${{ args.limit }}' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
|
|
3
|
+
cli({
|
|
4
|
+
site: 'binance',
|
|
5
|
+
name: 'trades',
|
|
6
|
+
description: 'Recent trades for a trading pair',
|
|
7
|
+
domain: 'data-api.binance.vision',
|
|
8
|
+
strategy: Strategy.PUBLIC,
|
|
9
|
+
browser: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
12
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of trades (max 1000)' },
|
|
13
|
+
],
|
|
14
|
+
columns: ['id', 'price', 'qty', 'quote_qty', 'buyer_maker'],
|
|
15
|
+
pipeline: [
|
|
16
|
+
{ fetch: { url: 'https://data-api.binance.vision/api/v3/trades?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
|
|
17
|
+
{ map: { id: '${{ item.id }}', price: '${{ item.price }}', qty: '${{ item.qty }}', quote_qty: '${{ item.quoteQty }}', buyer_maker: '${{ item.isBuyerMaker }}' } },
|
|
18
|
+
{ limit: '${{ args.limit }}' },
|
|
19
|
+
],
|
|
20
|
+
});
|
package/clis/boss/utils.js
CHANGED
|
@@ -214,7 +214,8 @@ export async function typeAndSendMessage(page, text) {
|
|
|
214
214
|
return true;
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
217
|
-
* Verbose log helper — prints when OPENCLI_VERBOSE
|
|
217
|
+
* Verbose log helper — prints when OPENCLI_VERBOSE is set, with DEBUG=opencli
|
|
218
|
+
* kept as a compatibility fallback.
|
|
218
219
|
*/
|
|
219
220
|
export function verbose(msg) {
|
|
220
221
|
if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
|