@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
|
|
4
|
+
const WEIXIN_DOMAIN = 'mp.weixin.qq.com';
|
|
5
|
+
|
|
6
|
+
export const draftsCommand = cli({
|
|
7
|
+
site: 'weixin',
|
|
8
|
+
name: 'drafts',
|
|
9
|
+
description: '列出微信公众号草稿箱',
|
|
10
|
+
domain: WEIXIN_DOMAIN,
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
timeoutSeconds: 60,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'limit', type: 'int', default: 10, help: '最多显示条数' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['Index', 'Title', 'Time'],
|
|
19
|
+
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
await page.goto('https://mp.weixin.qq.com/');
|
|
22
|
+
await page.wait(3);
|
|
23
|
+
const token = await page.evaluate(`(window.location.href.match(/token=(\\d+)/)||[])[1]`);
|
|
24
|
+
if (!token) {
|
|
25
|
+
throw new AuthRequiredError(WEIXIN_DOMAIN, '微信公众号草稿箱需要已登录的 mp.weixin.qq.com 会话');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await page.goto(`https://mp.weixin.qq.com/cgi-bin/appmsg?begin=0&count=${kwargs.limit}&type=77&action=list_card&token=${token}&lang=zh_CN`);
|
|
29
|
+
await page.wait(4);
|
|
30
|
+
|
|
31
|
+
const drafts = await page.evaluate(`(() => {
|
|
32
|
+
var results = [];
|
|
33
|
+
var idx = 0;
|
|
34
|
+
|
|
35
|
+
var cards = document.querySelectorAll('.weui-desktop-card');
|
|
36
|
+
for (var i = 0; i < cards.length; i++) {
|
|
37
|
+
if (cards[i].className.includes('card_new')) continue;
|
|
38
|
+
var titleEl = cards[i].querySelector('[class*=title]');
|
|
39
|
+
var timeEl = cards[i].querySelector('[class*=tips]');
|
|
40
|
+
var title = titleEl ? titleEl.textContent.trim() : '';
|
|
41
|
+
var time = timeEl ? timeEl.textContent.trim().replace(/\\s+/g, ' ') : '';
|
|
42
|
+
if (title) results.push({ Index: ++idx, Title: title, Time: time });
|
|
43
|
+
}
|
|
44
|
+
if (results.length > 0) return results;
|
|
45
|
+
|
|
46
|
+
var rows = document.querySelectorAll('tr, [class*=appmsg_item], [class*=list_item]');
|
|
47
|
+
rows.forEach(function(row) {
|
|
48
|
+
var titleEl = row.querySelector('[class*=title] a, [class*=title], h4');
|
|
49
|
+
var timeEl = row.querySelector('[class*=time], td:nth-child(2)');
|
|
50
|
+
var title = titleEl ? titleEl.textContent.trim() : '';
|
|
51
|
+
var time = timeEl ? timeEl.textContent.trim() : '';
|
|
52
|
+
if (title && title !== '内容' && title.length < 80) {
|
|
53
|
+
results.push({ Index: ++idx, Title: title, Time: time });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return results;
|
|
57
|
+
})()`);
|
|
58
|
+
|
|
59
|
+
if (!drafts || drafts.length === 0) {
|
|
60
|
+
throw new EmptyResultError('weixin drafts', 'No structured drafts found in the current Weixin Official Account backend');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return drafts.slice(0, kwargs.limit);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import './create-draft.js';
|
|
5
|
+
import './drafts.js';
|
|
6
|
+
|
|
7
|
+
function createPageMock(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
evaluate: overrides.evaluate ?? vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('weixin command registration', () => {
|
|
17
|
+
it('registers create-draft and drafts commands', () => {
|
|
18
|
+
const registry = getRegistry();
|
|
19
|
+
const values = [...registry.values()];
|
|
20
|
+
expect(values.find(c => c.site === 'weixin' && c.name === 'create-draft')).toBeDefined();
|
|
21
|
+
expect(values.find(c => c.site === 'weixin' && c.name === 'drafts')).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('weixin drafts command', () => {
|
|
26
|
+
it('throws AuthRequiredError when no session token is available', async () => {
|
|
27
|
+
const command = getRegistry().get('weixin/drafts');
|
|
28
|
+
const page = createPageMock({
|
|
29
|
+
evaluate: vi.fn().mockResolvedValueOnce(undefined),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(command.func(page, { limit: 10 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('fails instead of scraping arbitrary body text when structured selectors miss', async () => {
|
|
36
|
+
const command = getRegistry().get('weixin/drafts');
|
|
37
|
+
const evaluate = vi.fn()
|
|
38
|
+
.mockResolvedValueOnce('123456')
|
|
39
|
+
.mockImplementationOnce(async (script) => {
|
|
40
|
+
expect(script).not.toContain('document.body.innerText');
|
|
41
|
+
return [];
|
|
42
|
+
});
|
|
43
|
+
const page = createPageMock({ evaluate });
|
|
44
|
+
|
|
45
|
+
await expect(command.func(page, { limit: 10 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns structured drafts and respects the requested limit', async () => {
|
|
49
|
+
const command = getRegistry().get('weixin/drafts');
|
|
50
|
+
const page = createPageMock({
|
|
51
|
+
evaluate: vi.fn()
|
|
52
|
+
.mockResolvedValueOnce('123456')
|
|
53
|
+
.mockResolvedValueOnce([
|
|
54
|
+
{ Index: 1, Title: '第一篇草稿', Time: '2026-04-24 10:00' },
|
|
55
|
+
{ Index: 2, Title: '第二篇草稿', Time: '2026-04-24 11:00' },
|
|
56
|
+
]),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await command.func(page, { limit: 1 });
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual([
|
|
62
|
+
{ Index: 1, Title: '第一篇草稿', Time: '2026-04-24 10:00' },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/clis/weread/ranking.js
CHANGED
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'limit', type: 'int', default: 20, help: 'Max results' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['rank', 'title', 'author', 'category', 'readingCount', 'bookId'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (args) => {
|
|
16
16
|
const cat = encodeURIComponent(args.category ?? 'all');
|
|
17
17
|
const data = await fetchWebApi(`/bookListInCategory/${cat}`, { rank: '1' });
|
|
18
18
|
const books = data?.books ?? [];
|
|
@@ -35,7 +35,7 @@ describe('weread/search regression', () => {
|
|
|
35
35
|
`),
|
|
36
36
|
});
|
|
37
37
|
vi.stubGlobal('fetch', fetchMock);
|
|
38
|
-
const result = await command.func(
|
|
38
|
+
const result = await command.func({ query: 'deep work', limit: 5 });
|
|
39
39
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
40
40
|
expect(String(fetchMock.mock.calls[0][0])).toContain('keyword=deep+work');
|
|
41
41
|
expect(String(fetchMock.mock.calls[1][0])).toContain('/web/search/books?keyword=deep+work');
|
|
@@ -72,7 +72,7 @@ describe('weread/search regression', () => {
|
|
|
72
72
|
text: () => Promise.resolve('<html><body><p>no search cards</p></body></html>'),
|
|
73
73
|
});
|
|
74
74
|
vi.stubGlobal('fetch', fetchMock);
|
|
75
|
-
const result = await command.func(
|
|
75
|
+
const result = await command.func({ query: 'deep work', limit: 5 });
|
|
76
76
|
expect(result).toEqual([
|
|
77
77
|
{
|
|
78
78
|
rank: 1,
|
|
@@ -128,7 +128,7 @@ describe('weread/search regression', () => {
|
|
|
128
128
|
`),
|
|
129
129
|
});
|
|
130
130
|
vi.stubGlobal('fetch', fetchMock);
|
|
131
|
-
const result = await command.func(
|
|
131
|
+
const result = await command.func({ query: 'cal newport', limit: 5 });
|
|
132
132
|
expect(result).toEqual([
|
|
133
133
|
{
|
|
134
134
|
rank: 1,
|
|
@@ -166,7 +166,7 @@ describe('weread/search regression', () => {
|
|
|
166
166
|
})
|
|
167
167
|
.mockRejectedValueOnce(new Error('network timeout'));
|
|
168
168
|
vi.stubGlobal('fetch', fetchMock);
|
|
169
|
-
const result = await command.func(
|
|
169
|
+
const result = await command.func({ query: 'deep work', limit: 5 });
|
|
170
170
|
expect(result).toEqual([
|
|
171
171
|
{
|
|
172
172
|
rank: 1,
|
|
@@ -220,7 +220,7 @@ describe('weread/search regression', () => {
|
|
|
220
220
|
`),
|
|
221
221
|
});
|
|
222
222
|
vi.stubGlobal('fetch', fetchMock);
|
|
223
|
-
const result = await command.func(
|
|
223
|
+
const result = await command.func({ query: '文明', limit: 5 });
|
|
224
224
|
expect(result).toEqual([
|
|
225
225
|
{
|
|
226
226
|
rank: 1,
|
|
@@ -279,7 +279,7 @@ describe('weread/search regression', () => {
|
|
|
279
279
|
`),
|
|
280
280
|
});
|
|
281
281
|
vi.stubGlobal('fetch', fetchMock);
|
|
282
|
-
const result = await command.func(
|
|
282
|
+
const result = await command.func({ query: '文明', limit: 5 });
|
|
283
283
|
expect(result).toEqual([
|
|
284
284
|
{
|
|
285
285
|
rank: 1,
|
|
@@ -332,7 +332,7 @@ describe('weread/search regression', () => {
|
|
|
332
332
|
`),
|
|
333
333
|
});
|
|
334
334
|
vi.stubGlobal('fetch', fetchMock);
|
|
335
|
-
const result = await command.func(
|
|
335
|
+
const result = await command.func({ query: '文明', limit: 5 });
|
|
336
336
|
expect(result).toEqual([
|
|
337
337
|
{
|
|
338
338
|
rank: 1,
|
|
@@ -386,7 +386,7 @@ describe('weread/search regression', () => {
|
|
|
386
386
|
`),
|
|
387
387
|
});
|
|
388
388
|
vi.stubGlobal('fetch', fetchMock);
|
|
389
|
-
const result = await command.func(
|
|
389
|
+
const result = await command.func({ query: '文明', limit: 5 });
|
|
390
390
|
expect(result).toEqual([
|
|
391
391
|
{
|
|
392
392
|
rank: 1,
|
package/clis/weread/search.js
CHANGED
|
@@ -124,7 +124,7 @@ cli({
|
|
|
124
124
|
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
|
125
125
|
],
|
|
126
126
|
columns: ['rank', 'title', 'author', 'bookId', 'url'],
|
|
127
|
-
func: async (
|
|
127
|
+
func: async (args) => {
|
|
128
128
|
const [data, htmlEntries] = await Promise.all([
|
|
129
129
|
fetchWebApi('/search/global', { keyword: args.query }),
|
|
130
130
|
loadSearchHtmlEntries(String(args.query ?? '')),
|
package/clis/wikipedia/random.js
CHANGED
|
@@ -9,7 +9,7 @@ cli({
|
|
|
9
9
|
browser: false,
|
|
10
10
|
args: [{ name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' }],
|
|
11
11
|
columns: ['title', 'description', 'extract', 'url'],
|
|
12
|
-
func: async (
|
|
12
|
+
func: async (args) => {
|
|
13
13
|
const lang = args.lang || 'en';
|
|
14
14
|
const data = (await wikiFetch(lang, '/api/rest_v1/page/random/summary'));
|
|
15
15
|
if (!data?.title)
|
package/clis/wikipedia/search.js
CHANGED
|
@@ -13,7 +13,7 @@ cli({
|
|
|
13
13
|
{ name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
|
|
14
14
|
],
|
|
15
15
|
columns: ['title', 'snippet', 'url'],
|
|
16
|
-
func: async (
|
|
16
|
+
func: async (args) => {
|
|
17
17
|
const limit = Math.max(1, Math.min(Number(args.limit), 50));
|
|
18
18
|
const lang = args.lang || 'en';
|
|
19
19
|
const q = encodeURIComponent(args.query);
|
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['title', 'description', 'extract', 'url'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (args) => {
|
|
16
16
|
const lang = args.lang || 'en';
|
|
17
17
|
const title = encodeURIComponent(args.title.replace(/ /g, '_'));
|
|
18
18
|
const data = (await wikiFetch(lang, `/api/rest_v1/page/summary/${title}`));
|
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
|
|
13
13
|
],
|
|
14
14
|
columns: ['rank', 'title', 'description', 'views'],
|
|
15
|
-
func: async (
|
|
15
|
+
func: async (args) => {
|
|
16
16
|
const lang = args.lang || 'en';
|
|
17
17
|
const limit = Math.max(1, Math.min(Number(args.limit), 50));
|
|
18
18
|
// Use yesterday's UTC date — Wikipedia API expects UTC and yesterday
|
package/clis/xianyu/chat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthRequiredError,
|
|
1
|
+
import { AuthRequiredError, selectorError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
3
|
import { normalizeNumericId } from './utils.js';
|
|
4
4
|
function buildChatUrl(itemId, peerUserId) {
|
|
@@ -105,7 +105,7 @@ cli({
|
|
|
105
105
|
throw new AuthRequiredError('www.goofish.com', 'Xianyu chat requires a logged-in browser session');
|
|
106
106
|
}
|
|
107
107
|
if (!state?.can_input) {
|
|
108
|
-
throw
|
|
108
|
+
throw selectorError('闲鱼聊天输入框', '未找到可用的聊天输入框,请确认该会话页已正确加载');
|
|
109
109
|
}
|
|
110
110
|
if (!text) {
|
|
111
111
|
return [{
|
|
@@ -123,7 +123,7 @@ cli({
|
|
|
123
123
|
}
|
|
124
124
|
const sent = await page.evaluate(buildSendMessageEvaluate(text));
|
|
125
125
|
if (!sent?.ok) {
|
|
126
|
-
throw
|
|
126
|
+
throw selectorError('闲鱼发送按钮', `消息发送失败:${sent?.reason || 'unknown-reason'}`);
|
|
127
127
|
}
|
|
128
128
|
await page.wait(1);
|
|
129
129
|
return [{
|
package/clis/xianyu/item.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
|
import { normalizeNumericId } from './utils.js';
|
|
4
4
|
function buildItemUrl(itemId) {
|
|
@@ -127,7 +127,7 @@ cli({
|
|
|
127
127
|
throw new EmptyResultError('xianyu item', 'Xianyu item detail is blocked by verification or risk control');
|
|
128
128
|
}
|
|
129
129
|
if (result?.error === 'mtop-not-ready') {
|
|
130
|
-
throw
|
|
130
|
+
throw selectorError('window.lib.mtop', '闲鱼页面未完成初始化,无法调用商品详情接口');
|
|
131
131
|
}
|
|
132
132
|
if (!result || typeof result !== 'object') {
|
|
133
133
|
throw new EmptyResultError('xianyu item', '闲鱼商品详情接口未返回有效数据');
|
package/clis/xianyu/item.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { AuthRequiredError, EmptyResultError
|
|
2
|
+
import { AuthRequiredError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
4
|
import { __test__ } from './item.js';
|
|
5
5
|
import './item.js';
|
|
@@ -49,8 +49,8 @@ describe('xianyu item command', () => {
|
|
|
49
49
|
const page = createPageMock({ error: 'blocked' });
|
|
50
50
|
await expect(command.func(page, { item_id: '1040754408976' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
51
51
|
});
|
|
52
|
-
it('keeps
|
|
52
|
+
it('keeps SELECTOR code for true mtop initialization failures', async () => {
|
|
53
53
|
const page = createPageMock({ error: 'mtop-not-ready' });
|
|
54
|
-
await expect(command.func(page, { item_id: '1040754408976' })).rejects.
|
|
54
|
+
await expect(command.func(page, { item_id: '1040754408976' })).rejects.toMatchObject({ code: 'SELECTOR' });
|
|
55
55
|
});
|
|
56
56
|
});
|
|
@@ -47,6 +47,11 @@ export function noteIdToDate(url) {
|
|
|
47
47
|
// Offset by UTC+8 (China Standard Time) so the date matches what XHS users see
|
|
48
48
|
return new Date((ts + 8 * 3600) * 1000).toISOString().slice(0, 10);
|
|
49
49
|
}
|
|
50
|
+
export function stripXhsAuthorDateSuffix(value) {
|
|
51
|
+
const text = (value || '').replace(/\s+/g, ' ').trim();
|
|
52
|
+
const stripped = text.replace(/\s*(?:\d{1,2}天前|\d+小时前|\d+分钟前|\d+秒前|刚刚|昨天|前天|\d+周前|\d+个月前|\d{1,2}-\d{1,2}|\d{4}-\d{1,2}-\d{1,2})$/u, '').trim();
|
|
53
|
+
return stripped || text;
|
|
54
|
+
}
|
|
50
55
|
cli({
|
|
51
56
|
site: 'xiaohongshu',
|
|
52
57
|
name: 'search',
|
|
@@ -81,6 +86,7 @@ cli({
|
|
|
81
86
|
};
|
|
82
87
|
|
|
83
88
|
const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
89
|
+
const stripXhsAuthorDateSuffix = ${stripXhsAuthorDateSuffix.toString()};
|
|
84
90
|
|
|
85
91
|
const results = [];
|
|
86
92
|
const seen = new Set();
|
|
@@ -90,7 +96,13 @@ cli({
|
|
|
90
96
|
if (el.classList.contains('query-note-item')) return;
|
|
91
97
|
|
|
92
98
|
const titleEl = el.querySelector('.title, .note-title, a.title, .footer .title span');
|
|
93
|
-
const nameEl = el.querySelector('a.author .name, .
|
|
99
|
+
const nameEl = el.querySelector('a.author .name, .author-name, .nick-name, .name');
|
|
100
|
+
const authorWrapEl = el.querySelector('a.author');
|
|
101
|
+
let author = cleanText(nameEl?.textContent || '');
|
|
102
|
+
if (!author && authorWrapEl) {
|
|
103
|
+
const nameChild = authorWrapEl.querySelector('.name');
|
|
104
|
+
author = nameChild ? cleanText(nameChild.textContent || '') : stripXhsAuthorDateSuffix(authorWrapEl.textContent || '');
|
|
105
|
+
}
|
|
94
106
|
const likesEl = el.querySelector('.count, .like-count, .like-wrapper .count');
|
|
95
107
|
// Prefer search_result link (preserves xsec_token) over generic /explore/ link
|
|
96
108
|
const detailLinkEl =
|
|
@@ -109,7 +121,7 @@ cli({
|
|
|
109
121
|
|
|
110
122
|
results.push({
|
|
111
123
|
title: cleanText(titleEl?.textContent || ''),
|
|
112
|
-
author
|
|
124
|
+
author,
|
|
113
125
|
likes: cleanText(likesEl?.textContent || '0'),
|
|
114
126
|
url,
|
|
115
127
|
author_url: normalizeUrl(authorLinkEl?.getAttribute('href') || ''),
|
|
@@ -130,3 +142,6 @@ cli({
|
|
|
130
142
|
}));
|
|
131
143
|
},
|
|
132
144
|
});
|
|
145
|
+
export const __test__ = {
|
|
146
|
+
stripXhsAuthorDateSuffix,
|
|
147
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
-
import {
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
import { __test__, noteIdToDate } from './search.js';
|
|
4
5
|
function createPageMock(evaluateResults) {
|
|
5
6
|
const evaluate = vi.fn();
|
|
6
7
|
for (const result of evaluateResults) {
|
|
@@ -127,6 +128,41 @@ describe('xiaohongshu search', () => {
|
|
|
127
128
|
// Two evaluate calls: wait + extraction
|
|
128
129
|
expect(page.evaluate).toHaveBeenCalledTimes(2);
|
|
129
130
|
});
|
|
131
|
+
it('separates fallback author text from appended relative date', async () => {
|
|
132
|
+
const cmd = getRegistry().get('xiaohongshu/search');
|
|
133
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
134
|
+
const dom = new JSDOM(`
|
|
135
|
+
<section class="note-item">
|
|
136
|
+
<a class="cover mask" href="/search_result/68e90be80000000004022e66?xsec_token=test-token"></a>
|
|
137
|
+
<div class="title">数字作者测试</div>
|
|
138
|
+
<a class="author" href="/user/profile/author123">
|
|
139
|
+
<span>数字3天前端</span><span>3天前</span>
|
|
140
|
+
</a>
|
|
141
|
+
<span class="count">8</span>
|
|
142
|
+
</section>
|
|
143
|
+
`, { url: 'https://www.xiaohongshu.com/search_result?keyword=test' });
|
|
144
|
+
const page = createPageMock([]);
|
|
145
|
+
page.evaluate.mockImplementationOnce(async () => 'content');
|
|
146
|
+
page.evaluate.mockImplementationOnce(async (script) => Function('document', `return (${script})`)(dom.window.document));
|
|
147
|
+
|
|
148
|
+
const result = await cmd.func(page, { query: '测试', limit: 1 });
|
|
149
|
+
|
|
150
|
+
expect(result[0]).toMatchObject({
|
|
151
|
+
title: '数字作者测试',
|
|
152
|
+
author: '数字3天前端',
|
|
153
|
+
likes: '8',
|
|
154
|
+
author_url: 'https://www.xiaohongshu.com/user/profile/author123',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('stripXhsAuthorDateSuffix', () => {
|
|
159
|
+
it('only strips trailing date suffixes and preserves date-like author text', () => {
|
|
160
|
+
expect(__test__.stripXhsAuthorDateSuffix('作者名 3天前')).toBe('作者名');
|
|
161
|
+
expect(__test__.stripXhsAuthorDateSuffix('作者名2026-04-01')).toBe('作者名');
|
|
162
|
+
expect(__test__.stripXhsAuthorDateSuffix('3天前端工程师')).toBe('3天前端工程师');
|
|
163
|
+
expect(__test__.stripXhsAuthorDateSuffix('刚刚好')).toBe('刚刚好');
|
|
164
|
+
expect(__test__.stripXhsAuthorDateSuffix('刚刚')).toBe('刚刚');
|
|
165
|
+
});
|
|
130
166
|
});
|
|
131
167
|
describe('noteIdToDate (ObjectID timestamp parsing)', () => {
|
|
132
168
|
it('parses a known note ID to the correct China-timezone date', () => {
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
{ name: 'output', default: './xiaoyuzhou-downloads', help: 'Output directory' },
|
|
19
19
|
],
|
|
20
20
|
columns: ['title', 'podcast', 'status', 'size', 'file'],
|
|
21
|
-
func: async (
|
|
21
|
+
func: async (args) => {
|
|
22
22
|
const credentials = loadXiaoyuzhouCredentials();
|
|
23
23
|
const response = await requestXiaoyuzhouJson('/v1/episode/get', {
|
|
24
24
|
query: { eid: args.id },
|
|
@@ -68,7 +68,7 @@ describe('xiaoyuzhou download', () => {
|
|
|
68
68
|
});
|
|
69
69
|
mockHttpDownload.mockResolvedValue({ success: true, size: 1234 });
|
|
70
70
|
|
|
71
|
-
const result = await cmd.func(
|
|
71
|
+
const result = await cmd.func({
|
|
72
72
|
id: 'ep123',
|
|
73
73
|
output: '/tmp/xiaoyuzhou-test',
|
|
74
74
|
});
|
|
@@ -106,7 +106,7 @@ describe('xiaoyuzhou download', () => {
|
|
|
106
106
|
});
|
|
107
107
|
mockHttpDownload.mockResolvedValue({ success: true, size: 2048 });
|
|
108
108
|
|
|
109
|
-
const result = await cmd.func(
|
|
109
|
+
const result = await cmd.func({
|
|
110
110
|
id: 'ep456',
|
|
111
111
|
output: '/tmp/xiaoyuzhou-test',
|
|
112
112
|
});
|
|
@@ -125,7 +125,7 @@ describe('xiaoyuzhou download', () => {
|
|
|
125
125
|
},
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
await expect(cmd.func(
|
|
128
|
+
await expect(cmd.func({ id: 'ep789', output: '/tmp/xiaoyuzhou-test' })).rejects.toMatchObject({
|
|
129
129
|
code: 'PARSE_ERROR',
|
|
130
130
|
message: 'Audio URL not found in episode payload',
|
|
131
131
|
hint: 'Episode payload does not expose media.source.url',
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
browser: false,
|
|
12
12
|
args: [{ name: 'id', positional: true, required: true, help: 'Episode ID (eid from podcast-episodes output)' }],
|
|
13
13
|
columns: ['title', 'podcast', 'duration', 'plays', 'comments', 'likes', 'date'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (args) => {
|
|
15
15
|
const credentials = loadXiaoyuzhouCredentials();
|
|
16
16
|
const response = await requestXiaoyuzhouJson('/v1/episode/get', {
|
|
17
17
|
query: { eid: args.id },
|
|
@@ -14,7 +14,7 @@ cli({
|
|
|
14
14
|
{ name: 'limit', type: 'int', default: 20, help: 'Max episodes to show' },
|
|
15
15
|
],
|
|
16
16
|
columns: ['eid', 'title', 'duration', 'plays', 'date'],
|
|
17
|
-
func: async (
|
|
17
|
+
func: async (args) => {
|
|
18
18
|
const requestedLimit = Number(args.limit);
|
|
19
19
|
if (!Number.isInteger(requestedLimit) || requestedLimit < 1) {
|
|
20
20
|
throw new CliError('INVALID_ARGUMENT', 'limit must be a positive integer', 'Example: --limit 5');
|
|
@@ -44,7 +44,7 @@ describe('xiaoyuzhou podcast-episodes', () => {
|
|
|
44
44
|
],
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
const result = await cmd.func(
|
|
47
|
+
const result = await cmd.func({
|
|
48
48
|
id: 'podcast-1',
|
|
49
49
|
limit: 3,
|
|
50
50
|
});
|
|
@@ -66,7 +66,7 @@ describe('xiaoyuzhou podcast-episodes', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it('rejects non-positive limits before hitting the API', async () => {
|
|
69
|
-
await expect(cmd.func(
|
|
69
|
+
await expect(cmd.func({
|
|
70
70
|
id: 'podcast-1',
|
|
71
71
|
limit: 0,
|
|
72
72
|
})).rejects.toMatchObject({
|
|
@@ -11,7 +11,7 @@ cli({
|
|
|
11
11
|
browser: false,
|
|
12
12
|
args: [{ name: 'id', positional: true, required: true, help: 'Podcast ID (from xiaoyuzhoufm.com URL)' }],
|
|
13
13
|
columns: ['title', 'author', 'description', 'subscribers', 'episodes', 'updated'],
|
|
14
|
-
func: async (
|
|
14
|
+
func: async (args) => {
|
|
15
15
|
const credentials = loadXiaoyuzhouCredentials();
|
|
16
16
|
const response = await requestXiaoyuzhouJson('/v1/podcast/get', {
|
|
17
17
|
query: { pid: args.id },
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
{ name: 'text', type: 'boolean', default: true, help: 'Save extracted transcript text file' },
|
|
19
19
|
],
|
|
20
20
|
columns: ['title', 'podcast', 'status', 'segments', 'json_file', 'text_file'],
|
|
21
|
-
func: async (
|
|
21
|
+
func: async (kwargs) => {
|
|
22
22
|
if (kwargs.json === false && kwargs.text === false) {
|
|
23
23
|
throw new ArgumentError('At least one of --json or --text must be enabled', 'Example: opencli xiaoyuzhou transcript 69dd0c98e2c8be31551f6a33 --text true');
|
|
24
24
|
}
|
|
@@ -67,7 +67,7 @@ describe('xiaoyuzhou transcript', () => {
|
|
|
67
67
|
mockFetchTranscriptBody.mockResolvedValue(JSON.stringify({
|
|
68
68
|
segments: [{ text: 'hello' }, { text: 'world' }],
|
|
69
69
|
}));
|
|
70
|
-
const result = await cmd.func(
|
|
70
|
+
const result = await cmd.func({
|
|
71
71
|
id: 'ep123',
|
|
72
72
|
output: '/tmp/xiaoyuzhou-transcripts',
|
|
73
73
|
json: true,
|
|
@@ -112,7 +112,7 @@ describe('xiaoyuzhou transcript', () => {
|
|
|
112
112
|
},
|
|
113
113
|
});
|
|
114
114
|
mockFetchTranscriptBody.mockResolvedValue(JSON.stringify({ text: 'hello' }));
|
|
115
|
-
await cmd.func(
|
|
115
|
+
await cmd.func({
|
|
116
116
|
id: 'ep456',
|
|
117
117
|
output: '/tmp/xiaoyuzhou-transcripts',
|
|
118
118
|
json: false,
|
|
@@ -137,7 +137,7 @@ describe('xiaoyuzhou transcript', () => {
|
|
|
137
137
|
credentials: { access_token: 'access-1', refresh_token: 'refresh-1' },
|
|
138
138
|
data: {},
|
|
139
139
|
});
|
|
140
|
-
await expect(cmd.func(
|
|
140
|
+
await expect(cmd.func({
|
|
141
141
|
id: 'ep123',
|
|
142
142
|
output: '/tmp/xiaoyuzhou-transcripts',
|
|
143
143
|
json: true,
|
|
@@ -168,7 +168,7 @@ describe('xiaoyuzhou transcript', () => {
|
|
|
168
168
|
mockFetchTranscriptBody.mockResolvedValue(JSON.stringify({
|
|
169
169
|
segments: [{ startAt: 0, endAt: 1 }],
|
|
170
170
|
}));
|
|
171
|
-
await expect(cmd.func(
|
|
171
|
+
await expect(cmd.func({
|
|
172
172
|
id: 'ep123',
|
|
173
173
|
output: '/tmp/xiaoyuzhou-transcripts',
|
|
174
174
|
json: true,
|
|
@@ -181,7 +181,7 @@ describe('xiaoyuzhou transcript', () => {
|
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
it('rejects disabling both json and text outputs', async () => {
|
|
184
|
-
await expect(cmd.func(
|
|
184
|
+
await expect(cmd.func({
|
|
185
185
|
id: 'ep123',
|
|
186
186
|
output: '/tmp/xiaoyuzhou-transcripts',
|
|
187
187
|
json: false,
|
package/clis/yollomi/models.js
CHANGED
|
@@ -10,7 +10,7 @@ cli({
|
|
|
10
10
|
{ name: 'type', default: 'all', choices: ['all', 'image', 'video', 'tool'], help: 'Filter by model type' },
|
|
11
11
|
],
|
|
12
12
|
columns: ['type', 'model', 'credits', 'description'],
|
|
13
|
-
func: async (
|
|
13
|
+
func: async (kwargs) => {
|
|
14
14
|
const filter = kwargs.type;
|
|
15
15
|
const rows = [];
|
|
16
16
|
if (filter === 'all' || filter === 'image') {
|
package/clis/youtube/channel.js
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
5
|
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
6
|
+
|
|
7
|
+
export function extractSelectedRichGridContents(browseData) {
|
|
8
|
+
const tabs = browseData?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
|
|
9
|
+
const readRichGrid = (tab) => tab?.tabRenderer?.content?.richGridRenderer?.contents;
|
|
10
|
+
const selectedTab = tabs.find(t => t?.tabRenderer?.selected);
|
|
11
|
+
const selectedContents = readRichGrid(selectedTab);
|
|
12
|
+
if (Array.isArray(selectedContents))
|
|
13
|
+
return selectedContents;
|
|
14
|
+
const fallbackContents = readRichGrid(tabs.find(t => {
|
|
15
|
+
const contents = readRichGrid(t);
|
|
16
|
+
return Array.isArray(contents) && contents.length > 0;
|
|
17
|
+
})) || readRichGrid(tabs.find(t => Array.isArray(readRichGrid(t))));
|
|
18
|
+
return Array.isArray(fallbackContents) ? fallbackContents : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
cli({
|
|
7
22
|
site: 'youtube',
|
|
8
23
|
name: 'channel',
|
|
@@ -27,6 +42,7 @@ cli({
|
|
|
27
42
|
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
28
43
|
const context = cfg.INNERTUBE_CONTEXT;
|
|
29
44
|
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
45
|
+
const extractSelectedRichGridContents = ${extractSelectedRichGridContents.toString()};
|
|
30
46
|
|
|
31
47
|
// Resolve handle to browseId if needed
|
|
32
48
|
let browseId = channelId;
|
|
@@ -133,7 +149,10 @@ cli({
|
|
|
133
149
|
});
|
|
134
150
|
if (videosResp.ok) {
|
|
135
151
|
const videosData = await videosResp.json();
|
|
136
|
-
|
|
152
|
+
// The InnerTube response includes ALL tabs (Home/Videos/Shorts/...),
|
|
153
|
+
// not just the requested one. Prefer the selected tab, but keep
|
|
154
|
+
// older single-tab responses working when YouTube omits selected.
|
|
155
|
+
const richGrid = extractSelectedRichGridContents(videosData);
|
|
137
156
|
for (const item of richGrid) {
|
|
138
157
|
if (recentVideos.length >= limit) break;
|
|
139
158
|
const v = item.richItemRenderer?.content?.videoRenderer;
|
|
@@ -183,3 +202,7 @@ cli({
|
|
|
183
202
|
return rows;
|
|
184
203
|
},
|
|
185
204
|
});
|
|
205
|
+
|
|
206
|
+
export const __test__ = {
|
|
207
|
+
extractSelectedRichGridContents,
|
|
208
|
+
};
|