@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
|
@@ -2,44 +2,41 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
3
|
import './follow.js';
|
|
4
4
|
describe('zhihu follow', () => {
|
|
5
|
-
it('
|
|
5
|
+
it('registers as a cookie browser command', () => {
|
|
6
6
|
const cmd = getRegistry().get('zhihu/follow');
|
|
7
|
-
expect(cmd
|
|
8
|
-
|
|
9
|
-
await expect(cmd.func(page, { target: 'question:123' })).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
|
10
|
-
expect(page.goto).not.toHaveBeenCalled();
|
|
11
|
-
expect(page.evaluate).not.toHaveBeenCalled();
|
|
7
|
+
expect(cmd).toBeDefined();
|
|
8
|
+
expect(cmd.strategy).toBe('cookie');
|
|
12
9
|
});
|
|
13
|
-
it('
|
|
10
|
+
it('follows via API and returns result', async () => {
|
|
14
11
|
const cmd = getRegistry().get('zhihu/follow');
|
|
15
12
|
const page = {
|
|
16
13
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
17
|
-
|
|
14
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate: vi.fn().mockResolvedValueOnce({ ok: true }),
|
|
18
16
|
};
|
|
19
|
-
await
|
|
20
|
-
|
|
21
|
-
});
|
|
17
|
+
const rows = await cmd.func(page, { target: 'question:123', execute: true });
|
|
18
|
+
expect(rows).toEqual([expect.objectContaining({ outcome: 'applied' })]);
|
|
22
19
|
});
|
|
23
|
-
it('
|
|
20
|
+
it('uses the parsed user slug for user follow API calls', async () => {
|
|
24
21
|
const cmd = getRegistry().get('zhihu/follow');
|
|
25
22
|
const page = {
|
|
26
23
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
27
|
-
|
|
24
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
evaluate: vi.fn().mockResolvedValueOnce({ ok: true }),
|
|
28
26
|
};
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
]);
|
|
27
|
+
await cmd.func(page, { target: 'user:alice', execute: true });
|
|
28
|
+
expect(page.evaluate.mock.calls[0][0]).toContain("'https://www.zhihu.com/api/v4/members/' + targetId + '/followers'");
|
|
29
|
+
expect(page.evaluate.mock.calls[0][0]).toContain('var targetId = "alice"');
|
|
30
|
+
expect(page.evaluate.mock.calls[0][0]).not.toContain('undefined');
|
|
32
31
|
});
|
|
33
|
-
it('
|
|
32
|
+
it('throws on API error', async () => {
|
|
34
33
|
const cmd = getRegistry().get('zhihu/follow');
|
|
35
34
|
const page = {
|
|
36
35
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
37
|
-
|
|
36
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
evaluate: vi.fn().mockResolvedValueOnce({ ok: false, message: 'already following' }),
|
|
38
38
|
};
|
|
39
|
-
await expect(cmd.func(page, { target: 'question:123', execute: true }))
|
|
40
|
-
code: '
|
|
41
|
-
});
|
|
42
|
-
expect(page.evaluate.mock.calls[0][0]).toContain('QuestionHeader');
|
|
43
|
-
expect(page.evaluate.mock.calls[0][0]).toContain('new Set(');
|
|
39
|
+
await expect(cmd.func(page, { target: 'question:123', execute: true }))
|
|
40
|
+
.rejects.toMatchObject({ code: 'COMMAND_EXEC' });
|
|
44
41
|
});
|
|
45
42
|
});
|
package/clis/zhihu/like.js
CHANGED
|
@@ -7,7 +7,7 @@ cli({
|
|
|
7
7
|
name: 'like',
|
|
8
8
|
description: 'Like a Zhihu answer or article',
|
|
9
9
|
domain: 'zhihu.com',
|
|
10
|
-
strategy: Strategy.
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
11
|
browser: true,
|
|
12
12
|
args: [
|
|
13
13
|
{ name: 'target', positional: true, required: true, help: 'Zhihu target URL or typed target' },
|
|
@@ -20,72 +20,27 @@ cli({
|
|
|
20
20
|
requireExecute(kwargs);
|
|
21
21
|
const rawTarget = String(kwargs.target);
|
|
22
22
|
const target = assertAllowedKinds('like', parseTarget(rawTarget));
|
|
23
|
-
await page.goto(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return /赞同|赞/.test(text) && node.hasAttribute('aria-pressed') && !inCommentItem;
|
|
44
|
-
});
|
|
45
|
-
if (candidates.length !== 1) return { state: 'ambiguous_answer_like' };
|
|
46
|
-
btn = candidates[0];
|
|
47
|
-
} else {
|
|
48
|
-
const articleRoot =
|
|
49
|
-
document.querySelector('article')
|
|
50
|
-
|| document.querySelector('.Post-Main')
|
|
51
|
-
|| document.querySelector('[itemprop="articleBody"]')
|
|
52
|
-
|| document;
|
|
53
|
-
const candidates = Array.from(articleRoot.querySelectorAll('button')).filter((node) => {
|
|
54
|
-
const text = (node.textContent || '').trim();
|
|
55
|
-
return /赞同|赞/.test(text) && node.hasAttribute('aria-pressed');
|
|
56
|
-
});
|
|
57
|
-
if (candidates.length !== 1) return { state: 'ambiguous_article_like' };
|
|
58
|
-
btn = candidates[0];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!btn) return { state: 'missing' };
|
|
62
|
-
if (btn.getAttribute('aria-pressed') === 'true') return { state: 'already_liked' };
|
|
63
|
-
|
|
64
|
-
btn.click();
|
|
65
|
-
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
66
|
-
|
|
67
|
-
return btn.getAttribute('aria-pressed') === 'true'
|
|
68
|
-
? { state: 'liked' }
|
|
69
|
-
: { state: 'unknown' };
|
|
70
|
-
})()`);
|
|
71
|
-
if (result?.state === 'wrong_answer') {
|
|
72
|
-
throw new CliError('TARGET_NOT_FOUND', 'Resolved answer target no longer matches the requested answer:<questionId>:<answerId>');
|
|
23
|
+
await page.goto('https://www.zhihu.com');
|
|
24
|
+
await page.wait(2);
|
|
25
|
+
const apiResult = await page.evaluate(`(async () => {
|
|
26
|
+
var targetKind = ${JSON.stringify(target.kind)};
|
|
27
|
+
var targetId = ${JSON.stringify(target.id)};
|
|
28
|
+
var resourceType = targetKind === 'answer' ? 'answers' : 'articles';
|
|
29
|
+
var url = 'https://www.zhihu.com/api/v4/' + resourceType + '/' + targetId + '/voters';
|
|
30
|
+
var resp = await fetch(url, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
credentials: 'include',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ type: 'up' }),
|
|
35
|
+
});
|
|
36
|
+
var data = await resp.json();
|
|
37
|
+
if (!resp.ok) return { ok: false, message: data.error ? data.error.message : 'unknown error' };
|
|
38
|
+
if (data && data.success === false) return { ok: false, message: 'Zhihu like API reported success=false' };
|
|
39
|
+
return { ok: true, success: data.success };
|
|
40
|
+
})()`);
|
|
41
|
+
if (!apiResult?.ok) {
|
|
42
|
+
throw new CliError('COMMAND_EXEC', apiResult?.message || 'Failed to like');
|
|
73
43
|
}
|
|
74
|
-
|
|
75
|
-
return buildResultRow(`Already liked ${target.kind}`, target.kind, rawTarget, 'already_applied');
|
|
76
|
-
}
|
|
77
|
-
if (result?.state === 'ambiguous_answer_like') {
|
|
78
|
-
throw new CliError('ACTION_NOT_AVAILABLE', 'Answer like control was not uniquely anchored on the requested answer');
|
|
79
|
-
}
|
|
80
|
-
if (result?.state === 'ambiguous_article_like') {
|
|
81
|
-
throw new CliError('ACTION_NOT_AVAILABLE', 'Article like control was not uniquely anchored on the requested target');
|
|
82
|
-
}
|
|
83
|
-
if (result?.state === 'missing') {
|
|
84
|
-
throw new CliError('ACTION_FAILED', 'Zhihu like control was missing before any write was dispatched');
|
|
85
|
-
}
|
|
86
|
-
if (result?.state !== 'liked') {
|
|
87
|
-
throw new CliError('OUTCOME_UNKNOWN', 'Zhihu like click was dispatched, but the final state could not be verified safely');
|
|
88
|
-
}
|
|
89
|
-
return buildResultRow(`Liked ${target.kind}`, target.kind, rawTarget, 'applied');
|
|
44
|
+
return buildResultRow(`Liked ${target.kind} ${target.id}`, target.kind, rawTarget, 'applied');
|
|
90
45
|
},
|
|
91
46
|
});
|
package/clis/zhihu/like.test.js
CHANGED
|
@@ -2,63 +2,40 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
3
|
import './like.js';
|
|
4
4
|
describe('zhihu like', () => {
|
|
5
|
-
it('
|
|
5
|
+
it('registers as a cookie browser command', () => {
|
|
6
6
|
const cmd = getRegistry().get('zhihu/like');
|
|
7
|
-
expect(cmd
|
|
8
|
-
|
|
9
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
10
|
-
evaluate: vi.fn().mockResolvedValue({ state: 'ambiguous_article_like' }),
|
|
11
|
-
};
|
|
12
|
-
await expect(cmd.func(page, { target: 'article:9', execute: true })).rejects.toMatchObject({
|
|
13
|
-
code: 'ACTION_NOT_AVAILABLE',
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
it('returns already_applied for an already-liked article target', async () => {
|
|
17
|
-
const cmd = getRegistry().get('zhihu/like');
|
|
18
|
-
const page = {
|
|
19
|
-
goto: vi.fn().mockResolvedValue(undefined),
|
|
20
|
-
evaluate: vi.fn().mockResolvedValue({ state: 'already_liked' }),
|
|
21
|
-
};
|
|
22
|
-
await expect(cmd.func(page, { target: 'article:9', execute: true })).resolves.toEqual([
|
|
23
|
-
expect.objectContaining({ outcome: 'already_applied', target_type: 'article', target: 'article:9' }),
|
|
24
|
-
]);
|
|
7
|
+
expect(cmd).toBeDefined();
|
|
8
|
+
expect(cmd.strategy).toBe('cookie');
|
|
25
9
|
});
|
|
26
|
-
it('
|
|
10
|
+
it('likes via API and returns result', async () => {
|
|
27
11
|
const cmd = getRegistry().get('zhihu/like');
|
|
28
12
|
const page = {
|
|
29
13
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
30
|
-
|
|
14
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
evaluate: vi.fn().mockResolvedValueOnce({ ok: true, success: true }),
|
|
31
16
|
};
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
]);
|
|
35
|
-
expect(page.goto).toHaveBeenCalledWith('https://www.zhihu.com/question/123/answer/456');
|
|
36
|
-
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
37
|
-
expect(page.evaluate.mock.calls[0][0]).toContain('targetQuestionId');
|
|
38
|
-
expect(page.evaluate.mock.calls[0][0]).toContain('"123"');
|
|
39
|
-
expect(page.evaluate.mock.calls[0][0]).toContain('"456"');
|
|
40
|
-
expect(page.evaluate.mock.calls[0][0]).toContain("node.getAttribute('data-answerid')");
|
|
41
|
-
expect(page.evaluate.mock.calls[0][0]).toContain("node.getAttribute('data-zop-question-answer')");
|
|
17
|
+
const rows = await cmd.func(page, { target: 'answer:1:2', execute: true });
|
|
18
|
+
expect(rows).toEqual([expect.objectContaining({ outcome: 'applied' })]);
|
|
42
19
|
});
|
|
43
|
-
it('
|
|
20
|
+
it('throws on API error', async () => {
|
|
44
21
|
const cmd = getRegistry().get('zhihu/like');
|
|
45
22
|
const page = {
|
|
46
23
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
47
|
-
|
|
24
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
evaluate: vi.fn().mockResolvedValueOnce({ ok: false, message: 'rate limited' }),
|
|
48
26
|
};
|
|
49
|
-
await expect(cmd.func(page, { target: 'answer:
|
|
50
|
-
code: '
|
|
51
|
-
});
|
|
27
|
+
await expect(cmd.func(page, { target: 'answer:1:2', execute: true }))
|
|
28
|
+
.rejects.toMatchObject({ code: 'COMMAND_EXEC' });
|
|
52
29
|
});
|
|
53
|
-
it('
|
|
30
|
+
it('does not treat success=false API responses as a successful like', async () => {
|
|
54
31
|
const cmd = getRegistry().get('zhihu/like');
|
|
55
32
|
const page = {
|
|
56
33
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
57
|
-
|
|
34
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
evaluate: vi.fn().mockResolvedValueOnce({ ok: false, message: 'Zhihu like API reported success=false' }),
|
|
58
36
|
};
|
|
59
|
-
await expect(cmd.func(page, { target: 'answer:
|
|
60
|
-
code: '
|
|
61
|
-
|
|
62
|
-
expect(page.evaluate.mock.calls[0][0]).toContain("if (!block) return { state: 'wrong_answer' }");
|
|
37
|
+
await expect(cmd.func(page, { target: 'answer:1:2', execute: true }))
|
|
38
|
+
.rejects.toMatchObject({ code: 'COMMAND_EXEC' });
|
|
39
|
+
expect(page.evaluate.mock.calls[0][0]).toContain('data.success === false');
|
|
63
40
|
});
|
|
64
41
|
});
|
package/clis/zhihu/search.js
CHANGED
|
@@ -15,12 +15,13 @@ cli({
|
|
|
15
15
|
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/<em>/g, '').replace(/<\\/em>/g, '').trim();
|
|
16
16
|
const keyword = \${{ args.query | json }};
|
|
17
17
|
const limit = \${{ args.limit }};
|
|
18
|
-
|
|
18
|
+
var fetchLimit = Math.max(limit * 3, 30);
|
|
19
|
+
const res = await fetch('https://www.zhihu.com/api/v4/search_v3?q=' + encodeURIComponent(keyword) + '&t=general&offset=0&limit=' + fetchLimit, {
|
|
19
20
|
credentials: 'include'
|
|
20
21
|
});
|
|
21
22
|
const d = await res.json();
|
|
22
23
|
return (d?.data || [])
|
|
23
|
-
.filter(item => item.type === '
|
|
24
|
+
.filter(item => item.object && (item.object.type === 'answer' || item.object.type === 'article' || item.object.type === 'question'))
|
|
24
25
|
.map(item => {
|
|
25
26
|
const obj = item.object || {};
|
|
26
27
|
const q = obj.question || {};
|
|
@@ -194,7 +194,14 @@ function buildResolveCurrentUserIdentityJs() {
|
|
|
194
194
|
|
|
195
195
|
const navScopes = Array.from(document.querySelectorAll(navScopeSelector));
|
|
196
196
|
const slug = findCurrentUserSlugFromRoots(navScopes, true) || findCurrentUserSlugFromRoots([document], false);
|
|
197
|
-
|
|
197
|
+
if (slug) return { slug };
|
|
198
|
+
|
|
199
|
+
var avatarImgs = document.querySelectorAll('header img[alt*="\\u4e3b\\u9875"]');
|
|
200
|
+
for (var ai = 0; ai < avatarImgs.length; ai++) {
|
|
201
|
+
var altMatch = (avatarImgs[ai].alt || '').match(/\\u70b9\\u51fb\\u6253\\u5f00(.+?)\\u7684\\u4e3b\\u9875/);
|
|
202
|
+
if (altMatch) return { slug: altMatch[1] };
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
198
205
|
})()`;
|
|
199
206
|
}
|
|
200
207
|
export async function resolveCurrentUserIdentity(page) {
|
|
@@ -163,6 +163,7 @@ describe('zhihu write shared helpers', () => {
|
|
|
163
163
|
const documentRoot = new FakeRoot({
|
|
164
164
|
'header, nav, [role="banner"], [role="navigation"]': [],
|
|
165
165
|
'a[href^="/people/"]': [],
|
|
166
|
+
'header img[alt*="\u4e3b\u9875"]': [],
|
|
166
167
|
});
|
|
167
168
|
const page = createPageForDom(documentRoot);
|
|
168
169
|
await expect(__test__.resolveCurrentUserIdentity(page)).rejects.toMatchObject({
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import {
|
|
5
|
+
buildSearchUrl,
|
|
6
|
+
normalizeZlibraryBookUrl,
|
|
7
|
+
} from './utils.js';
|
|
8
|
+
import './search.js';
|
|
9
|
+
import './info.js';
|
|
10
|
+
|
|
11
|
+
function createPageMock(evaluateResults = []) {
|
|
12
|
+
const evaluate = vi.fn();
|
|
13
|
+
for (const result of evaluateResults) {
|
|
14
|
+
evaluate.mockResolvedValueOnce(result);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
evaluate,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('zlibrary commands', () => {
|
|
24
|
+
it('registers search and info commands', () => {
|
|
25
|
+
expect(getRegistry().get('zlibrary/search')).toBeDefined();
|
|
26
|
+
expect(getRegistry().get('zlibrary/info')).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('normalizes search query and rejects empty searches', () => {
|
|
30
|
+
expect(buildSearchUrl(' test book ')).toBe('https://z-library.im/s/test%20book');
|
|
31
|
+
expect(() => buildSearchUrl(' ')).toThrow(ArgumentError);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('restricts info URLs to the configured zlibrary host', () => {
|
|
35
|
+
expect(normalizeZlibraryBookUrl('https://z-library.im/book/demo')).toBe('https://z-library.im/book/demo');
|
|
36
|
+
expect(normalizeZlibraryBookUrl('https://www.z-library.im/book/demo')).toBe('https://www.z-library.im/book/demo');
|
|
37
|
+
expect(() => normalizeZlibraryBookUrl('https://example.com/book/demo')).toThrow(ArgumentError);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('search fails fast on empty extraction results', async () => {
|
|
41
|
+
const command = getRegistry().get('zlibrary/search');
|
|
42
|
+
const page = createPageMock(['[]']);
|
|
43
|
+
|
|
44
|
+
await expect(command.func(page, { query: 'missing', limit: 10 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('info waits seconds, not milliseconds-as-seconds, before extracting formats', async () => {
|
|
48
|
+
const command = getRegistry().get('zlibrary/info');
|
|
49
|
+
const page = createPageMock([
|
|
50
|
+
'Demo Book',
|
|
51
|
+
undefined,
|
|
52
|
+
JSON.stringify({ pdf: 'https://z-library.im/dl/pdf', epub: '' }),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
await expect(command.func(page, { url: 'https://z-library.im/book/demo' })).resolves.toEqual([{
|
|
56
|
+
title: 'Demo Book',
|
|
57
|
+
pdf: 'https://z-library.im/dl/pdf',
|
|
58
|
+
epub: '',
|
|
59
|
+
url: 'https://z-library.im/book/demo',
|
|
60
|
+
}]);
|
|
61
|
+
expect(page.wait).toHaveBeenCalledWith({ time: 5 });
|
|
62
|
+
expect(page.wait).toHaveBeenCalledWith({ time: 3 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('info fails fast when formats are missing', async () => {
|
|
66
|
+
const command = getRegistry().get('zlibrary/info');
|
|
67
|
+
const page = createPageMock([
|
|
68
|
+
'Login Required',
|
|
69
|
+
undefined,
|
|
70
|
+
JSON.stringify({ pdf: '', epub: '' }),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
await expect(command.func(page, { url: 'https://z-library.im/book/demo' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { ZLIBRARY_DOMAIN, extractBookTitle, extractFormats, normalizeZlibraryBookUrl } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'zlibrary',
|
|
7
|
+
name: 'info',
|
|
8
|
+
description: 'Get book details and available download formats from a Z-Library book page',
|
|
9
|
+
domain: ZLIBRARY_DOMAIN,
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [
|
|
14
|
+
{
|
|
15
|
+
name: 'url',
|
|
16
|
+
positional: true,
|
|
17
|
+
required: true,
|
|
18
|
+
help: 'Z-Library book page URL (e.g. https://z-library.im/book/...)',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
columns: ['title', 'pdf', 'epub', 'url'],
|
|
22
|
+
func: async (page, args) => {
|
|
23
|
+
const url = normalizeZlibraryBookUrl(args.url);
|
|
24
|
+
|
|
25
|
+
await page.goto(url, { waitUntil: 'load', settleMs: 3000 });
|
|
26
|
+
await page.wait({ time: 5 });
|
|
27
|
+
|
|
28
|
+
const title = await extractBookTitle(page);
|
|
29
|
+
const formats = await extractFormats(page);
|
|
30
|
+
|
|
31
|
+
if (!title || (!formats.pdf && !formats.epub)) {
|
|
32
|
+
throw new EmptyResultError(
|
|
33
|
+
'zlibrary info',
|
|
34
|
+
'Could not extract a book title and download formats. Check the URL, login state, and whether Z-Library changed its page layout.',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
title,
|
|
41
|
+
pdf: formats.pdf || '',
|
|
42
|
+
epub: formats.epub || '',
|
|
43
|
+
url,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { ZLIBRARY_DOMAIN, buildSearchUrl, extractSearchResults } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'zlibrary',
|
|
7
|
+
name: 'search',
|
|
8
|
+
description: 'Search Z-Library for books by title, author, ISBN, or keyword',
|
|
9
|
+
domain: ZLIBRARY_DOMAIN,
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [
|
|
14
|
+
{
|
|
15
|
+
name: 'query',
|
|
16
|
+
positional: true,
|
|
17
|
+
required: true,
|
|
18
|
+
help: 'Search keyword (title, author, ISBN, etc.)',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'limit',
|
|
22
|
+
type: 'int',
|
|
23
|
+
default: 10,
|
|
24
|
+
help: 'Max results (1–25)',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
columns: ['rank', 'title', 'author', 'url'],
|
|
28
|
+
func: async (page, args) => {
|
|
29
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 25));
|
|
30
|
+
const searchUrl = buildSearchUrl(args.query);
|
|
31
|
+
|
|
32
|
+
await page.goto(searchUrl, { waitUntil: 'load', settleMs: 3000 });
|
|
33
|
+
await page.wait({ time: 5 });
|
|
34
|
+
|
|
35
|
+
const results = await extractSearchResults(page, limit);
|
|
36
|
+
|
|
37
|
+
if (!results.length) {
|
|
38
|
+
throw new EmptyResultError(
|
|
39
|
+
'zlibrary search',
|
|
40
|
+
'No books found. Try a different keyword or check that you are logged into Z-Library.',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return results;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Z-Library adapter utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
6
|
+
|
|
7
|
+
const ZLIBRARY_DOMAIN = 'z-library.im';
|
|
8
|
+
const ZLIBRARY_ORIGIN = `https://${ZLIBRARY_DOMAIN}`;
|
|
9
|
+
const ZLIBRARY_ALLOWED_HOSTS = new Set([
|
|
10
|
+
ZLIBRARY_DOMAIN,
|
|
11
|
+
`www.${ZLIBRARY_DOMAIN}`,
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function normalizeZlibraryBookUrl(input) {
|
|
15
|
+
const raw = String(input || '').trim();
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = new URL(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
throw new ArgumentError('Z-Library book URL must be a valid http(s) URL', `Example: ${ZLIBRARY_ORIGIN}/book/...`);
|
|
21
|
+
}
|
|
22
|
+
if (!['http:', 'https:'].includes(url.protocol) || !ZLIBRARY_ALLOWED_HOSTS.has(url.hostname)) {
|
|
23
|
+
throw new ArgumentError(
|
|
24
|
+
`Unsupported Z-Library URL host: ${url.hostname}`,
|
|
25
|
+
`Pass a book URL under ${ZLIBRARY_DOMAIN}, for example ${ZLIBRARY_ORIGIN}/book/...`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a Z-Library search URL.
|
|
33
|
+
* Z-Library uses /s/<url-encoded-query> for search.
|
|
34
|
+
*/
|
|
35
|
+
export function buildSearchUrl(query) {
|
|
36
|
+
const normalized = String(query || '').trim();
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
throw new ArgumentError('zlibrary search query cannot be empty');
|
|
39
|
+
}
|
|
40
|
+
return `${ZLIBRARY_ORIGIN}/s/${encodeURIComponent(normalized)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract book title from page context.
|
|
45
|
+
* Tries z-bookcard shadow DOM first, then falls back to page title.
|
|
46
|
+
*/
|
|
47
|
+
export async function extractBookTitle(page) {
|
|
48
|
+
const title = await page.evaluate(`
|
|
49
|
+
(() => {
|
|
50
|
+
const card = document.querySelector('z-bookcard');
|
|
51
|
+
if (card && card.shadowRoot) {
|
|
52
|
+
const el = card.shadowRoot.querySelector('[class*="title"], h1, a');
|
|
53
|
+
if (el) return el.textContent.trim().split('\\n')[0].trim();
|
|
54
|
+
}
|
|
55
|
+
return document.title.replace(/\\s*[-|].*$/, '').trim();
|
|
56
|
+
})()
|
|
57
|
+
`);
|
|
58
|
+
return String(title || '').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract available download formats from book page.
|
|
63
|
+
* Clicks the three-dot menu to reveal download options.
|
|
64
|
+
* NOTE: Z-Library download links redirect through /dl/<hash> URLs.
|
|
65
|
+
* These require browser cookies and may not produce direct file downloads
|
|
66
|
+
* in OpenCLI's browser automation. For actual file downloading,
|
|
67
|
+
* consider using Playwright's download event handling instead.
|
|
68
|
+
*/
|
|
69
|
+
export async function extractFormats(page) {
|
|
70
|
+
// Click three-dot menu if present
|
|
71
|
+
await page.evaluate(`
|
|
72
|
+
(() => {
|
|
73
|
+
const btn = document.querySelector(
|
|
74
|
+
'button[aria-label*="more" i], [class*="dots" i], [class*="more" i]'
|
|
75
|
+
);
|
|
76
|
+
if (btn) btn.click();
|
|
77
|
+
})()
|
|
78
|
+
`);
|
|
79
|
+
// Wait for menu
|
|
80
|
+
await page.wait({ time: 3 });
|
|
81
|
+
|
|
82
|
+
const formats = await page.evaluate(`
|
|
83
|
+
JSON.stringify((() => {
|
|
84
|
+
const res = { pdf: '', epub: '' };
|
|
85
|
+
document.querySelectorAll('a[href]').forEach(a => {
|
|
86
|
+
const h = a.href || '';
|
|
87
|
+
const t = (a.textContent || '').toUpperCase();
|
|
88
|
+
if (h.includes('/dl/') && t.includes('PDF')) res.pdf = h;
|
|
89
|
+
if (h.includes('/dl/') && t.includes('EPUB')) res.epub = h;
|
|
90
|
+
});
|
|
91
|
+
return res;
|
|
92
|
+
})())
|
|
93
|
+
`);
|
|
94
|
+
return JSON.parse(formats);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract book cards from search results page.
|
|
99
|
+
*
|
|
100
|
+
* Z-Library renders search results as <z-bookcard> custom elements.
|
|
101
|
+
* Each card contains the book title, author, and a link to the book page.
|
|
102
|
+
* The link is inside a shadow DOM that can be queried with card.shadowRoot.
|
|
103
|
+
*
|
|
104
|
+
* This approach was validated on 2026-04-28 against z-library.im.
|
|
105
|
+
*/
|
|
106
|
+
export async function extractSearchResults(page, limit) {
|
|
107
|
+
const raw = await page.evaluate(`
|
|
108
|
+
JSON.stringify(
|
|
109
|
+
Array.from(document.querySelectorAll('z-bookcard'))
|
|
110
|
+
.slice(0, ${limit})
|
|
111
|
+
.map((card, index) => {
|
|
112
|
+
const text = card.textContent.trim();
|
|
113
|
+
const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
|
|
114
|
+
const title = lines[0] || '';
|
|
115
|
+
const author = lines[1] || '';
|
|
116
|
+
let url = '';
|
|
117
|
+
try {
|
|
118
|
+
if (card.shadowRoot) {
|
|
119
|
+
const link = card.shadowRoot.querySelector('a');
|
|
120
|
+
if (link) url = link.href || '';
|
|
121
|
+
}
|
|
122
|
+
} catch(e) {}
|
|
123
|
+
return { rank: index + 1, title, author, url };
|
|
124
|
+
})
|
|
125
|
+
.filter(item => item.url && item.title)
|
|
126
|
+
)
|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(raw);
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { ZLIBRARY_DOMAIN, ZLIBRARY_ORIGIN };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { InternalCliCommand } from './registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the editable source file path for an adapter.
|
|
4
|
+
*
|
|
5
|
+
* Priority:
|
|
6
|
+
* 1. cmd.source (set for FS-scanned JS and manifest lazy-loaded JS)
|
|
7
|
+
* 2. cmd._modulePath (set for manifest lazy-loaded JS)
|
|
8
|
+
*
|
|
9
|
+
* Skip manifest: prefixed pseudo-paths (YAML commands inlined in manifest).
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveAdapterSourcePath(cmd: InternalCliCommand): string | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the editable source file path for an adapter.
|
|
4
|
+
*
|
|
5
|
+
* Priority:
|
|
6
|
+
* 1. cmd.source (set for FS-scanned JS and manifest lazy-loaded JS)
|
|
7
|
+
* 2. cmd._modulePath (set for manifest lazy-loaded JS)
|
|
8
|
+
*
|
|
9
|
+
* Skip manifest: prefixed pseudo-paths (YAML commands inlined in manifest).
|
|
10
|
+
*/
|
|
11
|
+
export function resolveAdapterSourcePath(cmd) {
|
|
12
|
+
const candidates = [];
|
|
13
|
+
if (cmd.source && !cmd.source.startsWith('manifest:')) {
|
|
14
|
+
candidates.push(cmd.source);
|
|
15
|
+
}
|
|
16
|
+
if (cmd._modulePath) {
|
|
17
|
+
candidates.push(cmd._modulePath);
|
|
18
|
+
}
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
if (fs.existsSync(candidate))
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
return candidates[0];
|
|
24
|
+
}
|