@jackwener/opencli 1.4.1 → 1.5.0
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/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +21 -1
- package/README.md +35 -6
- package/README.zh-CN.md +12 -5
- package/SKILL.md +2 -0
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1111 -112
- package/dist/cli.js +34 -3
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +2 -1
- package/dist/clis/twitter/search.test.js +2 -0
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +114 -18
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +119 -0
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +8 -14
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +9 -2
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +15 -32
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +21 -1
- package/dist/hooks.js +2 -0
- package/dist/main.js +5 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin.d.ts +38 -5
- package/dist/plugin.js +267 -33
- package/dist/plugin.test.js +220 -3
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.js +1 -1
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +1 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +6 -2
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +87 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/package.json +1 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +2 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +2 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +35 -3
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +2 -0
- package/src/clis/twitter/search.ts +3 -1
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +137 -0
- package/src/clis/xiaohongshu/publish.ts +129 -18
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +8 -15
- package/src/doctor.ts +13 -2
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/errors.ts +25 -66
- package/src/execution.ts +28 -3
- package/src/hooks.ts +1 -0
- package/src/main.ts +6 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin.test.ts +246 -2
- package/src/plugin.ts +338 -36
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +1 -1
- package/src/serialization.ts +4 -0
- package/src/types.ts +1 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +1 -1
- package/tests/e2e/browser-public.test.ts +62 -4
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +10 -0
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking update checker.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: register exit-hook + kick-off-background-fetch
|
|
5
|
+
* - On startup: kick off background fetch (non-blocking)
|
|
6
|
+
* - On process exit: read cache, print notice if newer version exists
|
|
7
|
+
* - Check interval: 24 hours
|
|
8
|
+
* - Notice appears AFTER command output, not before (same as npm/gh/yarn)
|
|
9
|
+
* - Never delays or blocks the CLI command
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { PKG_VERSION } from './version.js';
|
|
17
|
+
|
|
18
|
+
const CACHE_DIR = path.join(os.homedir(), '.opencli');
|
|
19
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
|
|
20
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
21
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@jackwener/opencli/latest';
|
|
22
|
+
|
|
23
|
+
interface UpdateCache {
|
|
24
|
+
lastCheck: number;
|
|
25
|
+
latestVersion: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Read cache once at module load — shared by both exported functions
|
|
29
|
+
const _cache: UpdateCache | null = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8')) as UpdateCache;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
function writeCache(latestVersion: string): void {
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
40
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ lastCheck: Date.now(), latestVersion }), 'utf-8');
|
|
41
|
+
} catch {
|
|
42
|
+
// Best-effort; never fail
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compare semver strings. Returns true if `a` is strictly newer than `b`. */
|
|
47
|
+
function isNewer(a: string, b: string): boolean {
|
|
48
|
+
const parse = (v: string) => v.replace(/^v/, '').split('-')[0].split('.').map(Number);
|
|
49
|
+
const pa = parse(a);
|
|
50
|
+
const pb = parse(b);
|
|
51
|
+
if (pa.some(isNaN) || pb.some(isNaN)) return false;
|
|
52
|
+
const [aMaj, aMin, aPat] = pa;
|
|
53
|
+
const [bMaj, bMin, bPat] = pb;
|
|
54
|
+
if (aMaj !== bMaj) return aMaj > bMaj;
|
|
55
|
+
if (aMin !== bMin) return aMin > bMin;
|
|
56
|
+
return aPat > bPat;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isCI(): boolean {
|
|
60
|
+
return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register a process exit hook that prints an update notice if a newer
|
|
65
|
+
* version was found on the last background check.
|
|
66
|
+
* Notice appears after command output — same pattern as npm/gh/yarn.
|
|
67
|
+
* Skipped during --get-completions to avoid polluting shell completion output.
|
|
68
|
+
*/
|
|
69
|
+
export function registerUpdateNoticeOnExit(): void {
|
|
70
|
+
if (isCI()) return;
|
|
71
|
+
if (process.argv.includes('--get-completions')) return;
|
|
72
|
+
|
|
73
|
+
process.on('exit', (code) => {
|
|
74
|
+
if (code !== 0) return; // Don't show update notice on error exit
|
|
75
|
+
if (!_cache) return;
|
|
76
|
+
if (!isNewer(_cache.latestVersion, PKG_VERSION)) return;
|
|
77
|
+
try {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
chalk.yellow(`\n Update available: v${PKG_VERSION} → v${_cache.latestVersion}\n`) +
|
|
80
|
+
chalk.dim(` Run: npm install -g @jackwener/opencli\n\n`),
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore broken pipe (stderr closed before process exits)
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Kick off a background fetch to npm registry. Writes to cache for next run.
|
|
90
|
+
* Fully non-blocking — never awaited.
|
|
91
|
+
*/
|
|
92
|
+
export function checkForUpdateBackground(): void {
|
|
93
|
+
if (isCI()) return;
|
|
94
|
+
if (_cache && Date.now() - _cache.lastCheck < CHECK_INTERVAL_MS) return;
|
|
95
|
+
|
|
96
|
+
void (async () => {
|
|
97
|
+
try {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
100
|
+
const res = await fetch(NPM_REGISTRY_URL, {
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
headers: { 'User-Agent': `opencli/${PKG_VERSION}` },
|
|
103
|
+
});
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (!res.ok) return;
|
|
106
|
+
const data = await res.json() as { version?: string };
|
|
107
|
+
if (typeof data.version === 'string') {
|
|
108
|
+
writeCache(data.version);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Network error: silently skip, try again next run
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
async function loadModule() {
|
|
4
|
+
return import('./clis/weixin/download.js');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
describe('weixin publish time extraction', () => {
|
|
8
|
+
it('prefers publish_time text over create_time-like date strings', async () => {
|
|
9
|
+
const mod = await loadModule();
|
|
10
|
+
|
|
11
|
+
expect(mod.extractWechatPublishTime(
|
|
12
|
+
'2026年3月24日 22:38',
|
|
13
|
+
'var create_time = "2026年3月24日 22:38";',
|
|
14
|
+
)).toBe('2026年3月24日 22:38');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('falls back to unix timestamp create_time values', async () => {
|
|
18
|
+
const mod = await loadModule();
|
|
19
|
+
|
|
20
|
+
expect(mod.extractWechatPublishTime(
|
|
21
|
+
'',
|
|
22
|
+
'var create_time = "1711291080";',
|
|
23
|
+
)).toBe('2024-03-24 22:38:00');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects malformed create_time values', async () => {
|
|
27
|
+
const mod = await loadModule();
|
|
28
|
+
|
|
29
|
+
expect(mod.extractWechatPublishTime(
|
|
30
|
+
'',
|
|
31
|
+
'var create_time = "2026年3月24日 22:38";',
|
|
32
|
+
)).toBe('');
|
|
33
|
+
expect(mod.extractWechatPublishTime(
|
|
34
|
+
'',
|
|
35
|
+
'var create_time = "1711291080abc";',
|
|
36
|
+
)).toBe('');
|
|
37
|
+
expect(mod.extractWechatPublishTime(
|
|
38
|
+
'',
|
|
39
|
+
'var create_time = "17112910800";',
|
|
40
|
+
)).toBe('');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('builds a self-contained browser helper that matches fallback behavior', async () => {
|
|
44
|
+
const mod = await loadModule();
|
|
45
|
+
|
|
46
|
+
const extractInPage = eval(mod.buildExtractWechatPublishTimeJs()) as (publishTimeText: string, htmlStr: string) => string;
|
|
47
|
+
|
|
48
|
+
expect(extractInPage(
|
|
49
|
+
'',
|
|
50
|
+
'var create_time = "1711291080";',
|
|
51
|
+
)).toBe('2024-03-24 22:38:00');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('browser helper still prefers DOM publish_time text', async () => {
|
|
55
|
+
const mod = await loadModule();
|
|
56
|
+
|
|
57
|
+
const extractInPage = eval(mod.buildExtractWechatPublishTimeJs()) as (publishTimeText: string, htmlStr: string) => string;
|
|
58
|
+
|
|
59
|
+
expect(extractInPage(
|
|
60
|
+
'2026年3月24日 22:38',
|
|
61
|
+
'var create_time = "1711291080";',
|
|
62
|
+
)).toBe('2026年3月24日 22:38');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from './registry.js';
|
|
3
|
+
import { fetchPrivateApi } from './clis/weread/utils.js';
|
|
4
|
+
import './clis/weread/shelf.js';
|
|
5
|
+
|
|
6
|
+
describe('weread private API regression', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('uses browser cookies and Node fetch for private API requests', async () => {
|
|
12
|
+
const mockPage = {
|
|
13
|
+
getCookies: vi.fn()
|
|
14
|
+
.mockResolvedValueOnce([
|
|
15
|
+
{ name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
|
|
16
|
+
{ name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
|
|
17
|
+
]),
|
|
18
|
+
evaluate: vi.fn(),
|
|
19
|
+
} as any;
|
|
20
|
+
|
|
21
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
json: () => Promise.resolve({ title: 'Test Book', errcode: 0 }),
|
|
25
|
+
});
|
|
26
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
27
|
+
|
|
28
|
+
const result = await fetchPrivateApi(mockPage, '/book/info', { bookId: '123' });
|
|
29
|
+
|
|
30
|
+
expect(result.title).toBe('Test Book');
|
|
31
|
+
expect(mockPage.getCookies).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(mockPage.getCookies).toHaveBeenCalledWith({ url: 'https://i.weread.qq.com/book/info?bookId=123' });
|
|
33
|
+
expect(mockPage.evaluate).not.toHaveBeenCalled();
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
'https://i.weread.qq.com/book/info?bookId=123',
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
headers: expect.objectContaining({
|
|
38
|
+
Cookie: 'wr_name=alice; wr_vid=vid123',
|
|
39
|
+
}),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('maps unauthenticated private API responses to AUTH_REQUIRED', async () => {
|
|
45
|
+
const mockPage = {
|
|
46
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
47
|
+
evaluate: vi.fn(),
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 401,
|
|
53
|
+
json: () => Promise.resolve({ errcode: -2010, errmsg: '用户不存在' }),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('Not logged in');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('maps non-auth API errors to API_ERROR', async () => {
|
|
60
|
+
const mockPage = {
|
|
61
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
62
|
+
evaluate: vi.fn(),
|
|
63
|
+
} as any;
|
|
64
|
+
|
|
65
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
66
|
+
ok: true,
|
|
67
|
+
status: 200,
|
|
68
|
+
json: () => Promise.resolve({ errcode: -1, errmsg: 'unknown error' }),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('unknown error');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('maps non-401 HTTP failures to FETCH_ERROR', async () => {
|
|
75
|
+
const mockPage = {
|
|
76
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
77
|
+
evaluate: vi.fn(),
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
81
|
+
ok: false,
|
|
82
|
+
status: 403,
|
|
83
|
+
json: () => Promise.resolve({ errmsg: 'forbidden' }),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('HTTP 403');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('maps invalid JSON to PARSE_ERROR', async () => {
|
|
90
|
+
const mockPage = {
|
|
91
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
92
|
+
evaluate: vi.fn(),
|
|
93
|
+
} as any;
|
|
94
|
+
|
|
95
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
96
|
+
ok: true,
|
|
97
|
+
status: 200,
|
|
98
|
+
json: () => Promise.reject(new SyntaxError('Unexpected token <')),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
await expect(fetchPrivateApi(mockPage, '/book/info')).rejects.toThrow('Invalid JSON');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('routes weread shelf through the private API helper path', async () => {
|
|
105
|
+
const command = getRegistry().get('weread/shelf');
|
|
106
|
+
expect(command?.func).toBeTypeOf('function');
|
|
107
|
+
|
|
108
|
+
const mockPage = {
|
|
109
|
+
getCookies: vi.fn()
|
|
110
|
+
.mockResolvedValueOnce([
|
|
111
|
+
{ name: 'wr_name', value: 'alice', domain: 'weread.qq.com' },
|
|
112
|
+
{ name: 'wr_vid', value: 'vid123', domain: 'i.weread.qq.com' },
|
|
113
|
+
]),
|
|
114
|
+
evaluate: vi.fn(),
|
|
115
|
+
} as any;
|
|
116
|
+
|
|
117
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
118
|
+
ok: true,
|
|
119
|
+
status: 200,
|
|
120
|
+
json: () => Promise.resolve({
|
|
121
|
+
books: [{
|
|
122
|
+
title: 'Deep Work',
|
|
123
|
+
author: 'Cal Newport',
|
|
124
|
+
readingProgress: 42,
|
|
125
|
+
bookId: 'abc123',
|
|
126
|
+
}],
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
130
|
+
|
|
131
|
+
const result = await command!.func!(mockPage, { limit: 1 });
|
|
132
|
+
|
|
133
|
+
expect(mockPage.evaluate).not.toHaveBeenCalled();
|
|
134
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
135
|
+
'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
|
|
136
|
+
expect.any(Object),
|
|
137
|
+
);
|
|
138
|
+
expect(mockPage.getCookies).toHaveBeenCalledWith({
|
|
139
|
+
url: 'https://i.weread.qq.com/shelf/sync?synckey=0&lectureSynckey=0',
|
|
140
|
+
});
|
|
141
|
+
expect(result).toEqual([
|
|
142
|
+
{
|
|
143
|
+
title: 'Deep Work',
|
|
144
|
+
author: 'Cal Newport',
|
|
145
|
+
progress: '42%',
|
|
146
|
+
bookId: 'abc123',
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/src/yaml-schema.ts
CHANGED
|
@@ -26,3 +26,23 @@ export interface YamlCliDefinition {
|
|
|
26
26
|
timeout?: number;
|
|
27
27
|
navigateBefore?: boolean | string;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
import type { Arg } from './registry.js';
|
|
31
|
+
|
|
32
|
+
/** Convert YAML args definition to the internal Arg[] format. */
|
|
33
|
+
export function parseYamlArgs(args: Record<string, YamlArgDefinition> | undefined): Arg[] {
|
|
34
|
+
if (!args || typeof args !== 'object') return [];
|
|
35
|
+
const result: Arg[] = [];
|
|
36
|
+
for (const [argName, argDef] of Object.entries(args)) {
|
|
37
|
+
result.push({
|
|
38
|
+
name: argName,
|
|
39
|
+
type: argDef?.type ?? 'str',
|
|
40
|
+
default: argDef?.default,
|
|
41
|
+
required: argDef?.required ?? false,
|
|
42
|
+
positional: argDef?.positional ?? false,
|
|
43
|
+
help: argDef?.description ?? argDef?.help ?? '',
|
|
44
|
+
choices: argDef?.choices,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -80,30 +80,34 @@ describe('login-required commands — graceful failure', () => {
|
|
|
80
80
|
}, 60_000);
|
|
81
81
|
|
|
82
82
|
// ── linux-do (requires login — all endpoints need authentication) ──
|
|
83
|
-
it('linux-do
|
|
84
|
-
await expectGracefulAuthFailure(['linux-do', '
|
|
85
|
-
}, 60_000);
|
|
86
|
-
|
|
87
|
-
it('linux-do latest fails gracefully without login', async () => {
|
|
88
|
-
await expectGracefulAuthFailure(['linux-do', 'latest', '--limit', '3', '-f', 'json'], 'linux-do latest');
|
|
83
|
+
it('linux-do feed fails gracefully without login', async () => {
|
|
84
|
+
await expectGracefulAuthFailure(['linux-do', 'feed', '--limit', '3', '-f', 'json'], 'linux-do feed');
|
|
89
85
|
}, 60_000);
|
|
90
86
|
|
|
91
87
|
it('linux-do categories fails gracefully without login', async () => {
|
|
92
88
|
await expectGracefulAuthFailure(['linux-do', 'categories', '--limit', '3', '-f', 'json'], 'linux-do categories');
|
|
93
89
|
}, 60_000);
|
|
94
90
|
|
|
95
|
-
it('linux-do
|
|
96
|
-
await expectGracefulAuthFailure(['linux-do', '
|
|
91
|
+
it('linux-do tags fails gracefully without login', async () => {
|
|
92
|
+
await expectGracefulAuthFailure(['linux-do', 'tags', '--limit', '3', '-f', 'json'], 'linux-do tags');
|
|
97
93
|
}, 60_000);
|
|
98
94
|
|
|
99
95
|
it('linux-do topic fails gracefully without login', async () => {
|
|
100
|
-
await expectGracefulAuthFailure(['linux-do', 'topic', '
|
|
96
|
+
await expectGracefulAuthFailure(['linux-do', 'topic', '1', '-f', 'json'], 'linux-do topic');
|
|
101
97
|
}, 60_000);
|
|
102
98
|
|
|
103
99
|
it('linux-do search fails gracefully without login', async () => {
|
|
104
100
|
await expectGracefulAuthFailure(['linux-do', 'search', 'test', '--limit', '3', '-f', 'json'], 'linux-do search');
|
|
105
101
|
}, 60_000);
|
|
106
102
|
|
|
103
|
+
it('linux-do user-topics fails gracefully without login', async () => {
|
|
104
|
+
await expectGracefulAuthFailure(['linux-do', 'user-topics', 'test', '--limit', '3', '-f', 'json'], 'linux-do user-topics');
|
|
105
|
+
}, 60_000);
|
|
106
|
+
|
|
107
|
+
it('linux-do user-posts fails gracefully without login', async () => {
|
|
108
|
+
await expectGracefulAuthFailure(['linux-do', 'user-posts', 'test', '--limit', '3', '-f', 'json'], 'linux-do user-posts');
|
|
109
|
+
}, 60_000);
|
|
110
|
+
|
|
107
111
|
// ── xiaohongshu (requires login) ──
|
|
108
112
|
it('xiaohongshu feed fails gracefully without login', async () => {
|
|
109
113
|
await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json'], 'xiaohongshu feed');
|
|
@@ -156,7 +156,7 @@ describe('browser extended public-data commands E2E', () => {
|
|
|
156
156
|
|
|
157
157
|
// ── yahoo-finance ──
|
|
158
158
|
it('yahoo-finance quote returns stock data', async () => {
|
|
159
|
-
const data = await tryBrowserCommand(['yahoo-finance', 'quote', '
|
|
159
|
+
const data = await tryBrowserCommand(['yahoo-finance', 'quote', 'AAPL', '-f', 'json']);
|
|
160
160
|
expectDataOrSkip(data, 'yahoo-finance quote');
|
|
161
161
|
}, 60_000);
|
|
162
162
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* E2E tests for core browser commands (bilibili, zhihu, v2ex).
|
|
2
|
+
* E2E tests for core browser commands (bilibili, zhihu, v2ex, IMDb).
|
|
3
3
|
* These use OPENCLI_HEADLESS=1 to launch a headless Chromium.
|
|
4
4
|
*
|
|
5
5
|
* NOTE: Some sites may block headless browsers with bot detection.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import { runCli, parseJsonOutput } from './helpers.js';
|
|
10
|
+
import { runCli, parseJsonOutput, type CliResult } from './helpers.js';
|
|
11
11
|
|
|
12
12
|
async function tryBrowserCommand(args: string[]): Promise<any[] | null> {
|
|
13
13
|
const { stdout, code } = await runCli(args, { timeout: 60_000 });
|
|
@@ -28,13 +28,47 @@ function expectDataOrSkip(data: any[] | null, label: string) {
|
|
|
28
28
|
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function isImdbChallenge(result: CliResult): boolean {
|
|
32
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
33
|
+
return /IMDb blocked this request|Robot Check|Are you a robot|verify that you are human|captcha/i.test(text);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isBrowserBridgeUnavailable(result: CliResult): boolean {
|
|
37
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
38
|
+
return /Browser Extension is not connected|Browser Bridge extension.*not connected|Daemon is running but the Browser Extension is not connected/i.test(text);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function expectImdbDataOrChallengeSkip(args: string[], label: string): Promise<any[] | null> {
|
|
42
|
+
const result = await runCli(args, { timeout: 60_000 });
|
|
43
|
+
if (result.code !== 0) {
|
|
44
|
+
if (isImdbChallenge(result)) {
|
|
45
|
+
console.warn(`${label}: skipped — IMDb challenge page detected`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (isBrowserBridgeUnavailable(result)) {
|
|
49
|
+
console.warn(`${label}: skipped — Browser Bridge extension is unavailable in this environment`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`${label} failed:\n${result.stderr || result.stdout}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const data = parseJsonOutput(result.stdout);
|
|
56
|
+
if (!Array.isArray(data)) {
|
|
57
|
+
throw new Error(`${label} returned non-array JSON:\n${result.stdout.slice(0, 500)}`);
|
|
58
|
+
}
|
|
59
|
+
if (data.length === 0) {
|
|
60
|
+
throw new Error(`${label} returned an empty result`);
|
|
61
|
+
}
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
describe('browser public-data commands E2E', () => {
|
|
32
66
|
|
|
33
67
|
// ── bilibili ──
|
|
34
68
|
it('bilibili hot returns trending videos', async () => {
|
|
35
69
|
const data = await tryBrowserCommand(['bilibili', 'hot', '--limit', '5', '-f', 'json']);
|
|
36
70
|
expectDataOrSkip(data, 'bilibili hot');
|
|
37
|
-
if (data) {
|
|
71
|
+
if (data?.length) {
|
|
38
72
|
expect(data[0]).toHaveProperty('title');
|
|
39
73
|
}
|
|
40
74
|
}, 60_000);
|
|
@@ -53,7 +87,7 @@ describe('browser public-data commands E2E', () => {
|
|
|
53
87
|
it('zhihu hot returns trending questions', async () => {
|
|
54
88
|
const data = await tryBrowserCommand(['zhihu', 'hot', '--limit', '5', '-f', 'json']);
|
|
55
89
|
expectDataOrSkip(data, 'zhihu hot');
|
|
56
|
-
if (data) {
|
|
90
|
+
if (data?.length) {
|
|
57
91
|
expect(data[0]).toHaveProperty('title');
|
|
58
92
|
}
|
|
59
93
|
}, 60_000);
|
|
@@ -68,4 +102,28 @@ describe('browser public-data commands E2E', () => {
|
|
|
68
102
|
const data = await tryBrowserCommand(['v2ex', 'daily', '--limit', '3', '-f', 'json']);
|
|
69
103
|
expectDataOrSkip(data, 'v2ex daily');
|
|
70
104
|
}, 60_000);
|
|
105
|
+
|
|
106
|
+
// ── imdb ──
|
|
107
|
+
it('imdb top returns chart data', async () => {
|
|
108
|
+
const data = await expectImdbDataOrChallengeSkip(['imdb', 'top', '--limit', '3', '-f', 'json'], 'imdb top');
|
|
109
|
+
if (data?.length) {
|
|
110
|
+
expect(data[0]).toHaveProperty('title');
|
|
111
|
+
}
|
|
112
|
+
}, 60_000);
|
|
113
|
+
|
|
114
|
+
it('imdb search returns results', async () => {
|
|
115
|
+
const data = await expectImdbDataOrChallengeSkip(['imdb', 'search', 'inception', '--limit', '3', '-f', 'json'], 'imdb search');
|
|
116
|
+
if (data?.length) {
|
|
117
|
+
expect(data[0]).toHaveProperty('id');
|
|
118
|
+
expect(data[0]).toHaveProperty('title');
|
|
119
|
+
}
|
|
120
|
+
}, 60_000);
|
|
121
|
+
|
|
122
|
+
it('imdb title returns movie details', async () => {
|
|
123
|
+
const data = await expectImdbDataOrChallengeSkip(['imdb', 'title', 'tt1375666', '-f', 'json'], 'imdb title');
|
|
124
|
+
if (data?.length) {
|
|
125
|
+
expect(data[0]).toHaveProperty('field');
|
|
126
|
+
expect(data[0]).toHaveProperty('value');
|
|
127
|
+
}
|
|
128
|
+
}, 60_000);
|
|
71
129
|
});
|
package/tests/e2e/helpers.ts
CHANGED
|
@@ -29,7 +29,8 @@ export async function runCli(
|
|
|
29
29
|
): Promise<CliResult> {
|
|
30
30
|
const timeout = opts.timeout ?? 30_000;
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
32
|
+
const runtime = process.env.OPENCLI_TEST_RUNTIME || 'node';
|
|
33
|
+
const { stdout, stderr } = await exec(runtime, [MAIN, ...args], {
|
|
33
34
|
cwd: ROOT,
|
|
34
35
|
timeout,
|
|
35
36
|
env: {
|
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import * as fs from 'node:fs/promises';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
7
10
|
import { parseJsonOutput, runCli } from './helpers.js';
|
|
8
11
|
|
|
9
12
|
function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean {
|
|
10
13
|
if (code === 0) return false;
|
|
11
14
|
// Overseas CI runners may get HTTP errors, geo-blocks, DNS failures,
|
|
12
|
-
// or receive mangled HTML that fails parsing.
|
|
15
|
+
// or receive mangled HTML that fails parsing. Some runners also fail
|
|
16
|
+
// without surfacing a useful stderr payload.
|
|
13
17
|
return /Error \[(FETCH_ERROR|PARSE_ERROR|NOT_FOUND)\]/.test(stderr)
|
|
14
|
-
|| /fetch failed/.test(stderr)
|
|
18
|
+
|| /fetch failed/.test(stderr)
|
|
19
|
+
|| stderr.trim() === '';
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
function isExpectedApplePodcastsRestriction(code: number, stderr: string): boolean {
|
|
@@ -126,6 +131,35 @@ describe('public commands E2E', () => {
|
|
|
126
131
|
expect(data[0]).toHaveProperty('id');
|
|
127
132
|
}, 30_000);
|
|
128
133
|
|
|
134
|
+
it('paperreview submit dry-run validates a local PDF without remote upload', async () => {
|
|
135
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencli-paperreview-'));
|
|
136
|
+
const pdfPath = path.join(tempDir, 'sample.pdf');
|
|
137
|
+
await fs.writeFile(pdfPath, Buffer.concat([Buffer.from('%PDF-1.4\n'), Buffer.alloc(256, 1)]));
|
|
138
|
+
|
|
139
|
+
const { stdout, code } = await runCli([
|
|
140
|
+
'paperreview',
|
|
141
|
+
'submit',
|
|
142
|
+
pdfPath,
|
|
143
|
+
'--email',
|
|
144
|
+
'wang2629651228@gmail.com',
|
|
145
|
+
'--venue',
|
|
146
|
+
'RAL',
|
|
147
|
+
'--dry-run',
|
|
148
|
+
'true',
|
|
149
|
+
'-f',
|
|
150
|
+
'json',
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
expect(code).toBe(0);
|
|
154
|
+
const data = parseJsonOutput(stdout);
|
|
155
|
+
expect(data).toMatchObject({
|
|
156
|
+
status: 'dry-run',
|
|
157
|
+
file: 'sample.pdf',
|
|
158
|
+
email: 'wang2629651228@gmail.com',
|
|
159
|
+
venue: 'RAL',
|
|
160
|
+
});
|
|
161
|
+
}, 30_000);
|
|
162
|
+
|
|
129
163
|
// ── hackernews ──
|
|
130
164
|
it('hackernews top returns structured data', async () => {
|
|
131
165
|
const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '3', '-f', 'json']);
|
|
@@ -235,7 +269,7 @@ describe('public commands E2E', () => {
|
|
|
235
269
|
|
|
236
270
|
it('v2ex topic returns topic detail', async () => {
|
|
237
271
|
// Topic 1000001 is a well-known V2EX topic
|
|
238
|
-
const { stdout, code } = await runCli(['v2ex', 'topic', '
|
|
272
|
+
const { stdout, code } = await runCli(['v2ex', 'topic', '1000001', '-f', 'json']);
|
|
239
273
|
// May fail if V2EX rate-limits, but should return structured data
|
|
240
274
|
if (code === 0) {
|
|
241
275
|
const data = parseJsonOutput(stdout);
|
|
@@ -38,7 +38,7 @@ describe('API health smoke tests', () => {
|
|
|
38
38
|
}, 30_000);
|
|
39
39
|
|
|
40
40
|
it('v2ex topic API is responsive', async () => {
|
|
41
|
-
const { stdout, code } = await runCli(['v2ex', 'topic', '
|
|
41
|
+
const { stdout, code } = await runCli(['v2ex', 'topic', '1000001', '-f', 'json']);
|
|
42
42
|
if (code === 0) {
|
|
43
43
|
const data = parseJsonOutput(stdout);
|
|
44
44
|
expect(data).toBeDefined();
|
package/vitest.config.ts
CHANGED
|
@@ -18,8 +18,18 @@ export default defineConfig({
|
|
|
18
18
|
name: 'adapter',
|
|
19
19
|
include: [
|
|
20
20
|
'src/clis/bilibili/**/*.test.ts',
|
|
21
|
+
'src/clis/imdb/**/*.test.ts',
|
|
22
|
+
'src/clis/jd/**/*.test.ts',
|
|
23
|
+
'src/clis/linux-do/**/*.test.ts',
|
|
24
|
+
'src/clis/xiaohongshu/**/*.test.ts',
|
|
25
|
+
'src/clis/twitter/**/*.test.ts',
|
|
26
|
+
'src/clis/douban/**/*.test.ts',
|
|
21
27
|
'src/clis/zhihu/**/*.test.ts',
|
|
22
28
|
'src/clis/v2ex/**/*.test.ts',
|
|
29
|
+
'src/clis/weread/**/*.test.ts',
|
|
30
|
+
'src/clis/36kr/**/*.test.ts',
|
|
31
|
+
'src/clis/producthunt/**/*.test.ts',
|
|
32
|
+
'src/clis/paperreview/**/*.test.ts',
|
|
23
33
|
],
|
|
24
34
|
sequence: { groupOrder: 1 },
|
|
25
35
|
},
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
site: linux-do
|
|
2
|
-
name: category
|
|
3
|
-
description: linux.do 分类内话题
|
|
4
|
-
domain: linux.do
|
|
5
|
-
browser: true
|
|
6
|
-
|
|
7
|
-
args:
|
|
8
|
-
slug:
|
|
9
|
-
positional: true
|
|
10
|
-
type: str
|
|
11
|
-
required: true
|
|
12
|
-
description: Category slug (use 'categories' command to find)
|
|
13
|
-
id:
|
|
14
|
-
positional: true
|
|
15
|
-
type: int
|
|
16
|
-
required: true
|
|
17
|
-
description: Category ID (use 'categories' command to find)
|
|
18
|
-
limit:
|
|
19
|
-
type: int
|
|
20
|
-
default: 20
|
|
21
|
-
description: Number of topics
|
|
22
|
-
|
|
23
|
-
pipeline:
|
|
24
|
-
- navigate: https://linux.do
|
|
25
|
-
|
|
26
|
-
- evaluate: |
|
|
27
|
-
(async () => {
|
|
28
|
-
const slug = ${{ args.slug | json }};
|
|
29
|
-
const res = await fetch('/c/' + encodeURIComponent(slug) + '/${{ args.id }}.json', { credentials: 'include' });
|
|
30
|
-
if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
|
|
31
|
-
let data;
|
|
32
|
-
try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
|
|
33
|
-
const topics = data?.topic_list?.topics || [];
|
|
34
|
-
return topics.slice(0, ${{ args.limit }}).map(t => ({
|
|
35
|
-
title: t.title,
|
|
36
|
-
replies: (t.posts_count || 1) - 1,
|
|
37
|
-
views: t.views,
|
|
38
|
-
likes: t.like_count,
|
|
39
|
-
}));
|
|
40
|
-
})()
|
|
41
|
-
|
|
42
|
-
- map:
|
|
43
|
-
rank: ${{ index + 1 }}
|
|
44
|
-
title: ${{ item.title }}
|
|
45
|
-
replies: ${{ item.replies }}
|
|
46
|
-
views: ${{ item.views }}
|
|
47
|
-
likes: ${{ item.likes }}
|
|
48
|
-
|
|
49
|
-
- limit: ${{ args.limit }}
|
|
50
|
-
|
|
51
|
-
columns: [rank, title, replies, views, likes]
|