@sovovs/bycli 2.1.7 → 2.1.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/cli-manifest.json
CHANGED
|
@@ -28107,49 +28107,6 @@
|
|
|
28107
28107
|
"sourceFile": "weixin/save-articles.js",
|
|
28108
28108
|
"navigateBefore": "https://mp.weixin.qq.com"
|
|
28109
28109
|
},
|
|
28110
|
-
{
|
|
28111
|
-
"site": "weixin",
|
|
28112
|
-
"name": "search",
|
|
28113
|
-
"description": "使用搜狗微信搜索公众号文章;如需导出正文 Markdown,请使用 weixin download 处理公众号文章链接",
|
|
28114
|
-
"access": "read",
|
|
28115
|
-
"domain": "weixin.sogou.com",
|
|
28116
|
-
"strategy": "public",
|
|
28117
|
-
"browser": true,
|
|
28118
|
-
"args": [
|
|
28119
|
-
{
|
|
28120
|
-
"name": "query",
|
|
28121
|
-
"type": "str",
|
|
28122
|
-
"required": true,
|
|
28123
|
-
"positional": true,
|
|
28124
|
-
"help": "搜索关键词;如需正文 Markdown,请使用 weixin download 处理公众号文章链接"
|
|
28125
|
-
},
|
|
28126
|
-
{
|
|
28127
|
-
"name": "page",
|
|
28128
|
-
"type": "int",
|
|
28129
|
-
"default": 1,
|
|
28130
|
-
"required": false,
|
|
28131
|
-
"help": "结果页码,从 1 开始"
|
|
28132
|
-
},
|
|
28133
|
-
{
|
|
28134
|
-
"name": "limit",
|
|
28135
|
-
"type": "int",
|
|
28136
|
-
"default": 10,
|
|
28137
|
-
"required": false,
|
|
28138
|
-
"help": "返回条数,最大 10"
|
|
28139
|
-
}
|
|
28140
|
-
],
|
|
28141
|
-
"columns": [
|
|
28142
|
-
"rank",
|
|
28143
|
-
"page",
|
|
28144
|
-
"title",
|
|
28145
|
-
"url",
|
|
28146
|
-
"summary",
|
|
28147
|
-
"publish_time"
|
|
28148
|
-
],
|
|
28149
|
-
"type": "js",
|
|
28150
|
-
"modulePath": "weixin/search.js",
|
|
28151
|
-
"sourceFile": "weixin/search.js"
|
|
28152
|
-
},
|
|
28153
28110
|
{
|
|
28154
28111
|
"site": "weread",
|
|
28155
28112
|
"name": "ai-outline",
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { AuthRequiredError, BrowserConnectError
|
|
1
|
+
import { AuthRequiredError, BrowserConnectError } from '@sovovs/bycli/errors';
|
|
2
2
|
|
|
3
3
|
const DOMAIN = 'mp.weixin.qq.com';
|
|
4
4
|
const LOGIN_URL = `https://${DOMAIN}/`;
|
|
5
|
-
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
6
|
-
const POLL_INTERVAL_MS = 500;
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* @typedef {{token: string, cookie: string, fingerprint?: string}} WechatCredentials
|
|
10
8
|
* @typedef {{url: string | null, hasLoginUi: boolean}} PreflightState
|
|
11
|
-
* @typedef {{
|
|
9
|
+
* @typedef {{now?: () => number}} ResolveBrowserCredentialsOptions
|
|
12
10
|
* @typedef {Pick<import('@sovovs/bycli/types').IPage, 'evaluate' | 'getCookies' | 'goto' | 'wait' | 'focusWindow'>} AuthPage
|
|
13
11
|
*/
|
|
14
12
|
|
|
@@ -94,9 +92,7 @@ export async function resolveBrowserCredentials(page, options = {}) {
|
|
|
94
92
|
throw new BrowserConnectError('No browser page is connected for WeChat authentication');
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
98
95
|
const now = options.now ?? Date.now;
|
|
99
|
-
const startedAt = now();
|
|
100
96
|
let state = await readPreflight(page);
|
|
101
97
|
|
|
102
98
|
if (!isLoggedInPreflight(state)) {
|
|
@@ -110,14 +106,10 @@ export async function resolveBrowserCredentials(page, options = {}) {
|
|
|
110
106
|
await page.goto(LOGIN_URL);
|
|
111
107
|
await page.focusWindow();
|
|
112
108
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
await page.wait(Math.min(POLL_INTERVAL_MS, remainingMs) / 1000);
|
|
119
|
-
state = await readPreflight(page);
|
|
120
|
-
}
|
|
109
|
+
throw new AuthRequiredError(
|
|
110
|
+
DOMAIN,
|
|
111
|
+
'WeChat login is required. The login tab is open; complete QR-code login and run the command again.',
|
|
112
|
+
);
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
const finalUrl = state.url;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as defaultFs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
3
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
4
4
|
import { cleanMarkdownFilename, wechatArticleToMarkdown } from './markdown.js';
|
|
5
5
|
|
|
6
6
|
export const MAX_FILENAME_ATTEMPTS = 100;
|
|
@@ -121,7 +121,8 @@ export async function saveArticles({ articles, accountName, outputDir, fetchArti
|
|
|
121
121
|
let articleHtml;
|
|
122
122
|
try {
|
|
123
123
|
articleHtml = await fetchArticleHtml(article);
|
|
124
|
-
} catch {
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof AuthRequiredError) throw error;
|
|
125
126
|
rows.push({ title: article.title || '', url: article.url || '', status: 'failed', stage: 'download', saved: '', error: 'article download failed' });
|
|
126
127
|
continue;
|
|
127
128
|
}
|
package/clis/weixin/download.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
10
10
|
import { downloadArticle } from '@sovovs/bycli/download/article-download';
|
|
11
|
+
import { AuthRequiredError } from '@sovovs/bycli/errors';
|
|
11
12
|
import { buildExtractWechatArticleContentJs } from './_wechat/article-content.js';
|
|
12
13
|
export { extractWechatArticleContent } from './_wechat/article-content.js';
|
|
13
14
|
// ============================================================
|
|
@@ -249,14 +250,10 @@ cli({
|
|
|
249
250
|
})()
|
|
250
251
|
`);
|
|
251
252
|
if (data?.errorHint === 'environment verification required') {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
status: 'failed — verification required in WeChat browser page',
|
|
257
|
-
size: '-',
|
|
258
|
-
saved: '-',
|
|
259
|
-
}];
|
|
253
|
+
throw new AuthRequiredError(
|
|
254
|
+
'mp.weixin.qq.com',
|
|
255
|
+
'WeChat article page requires environment verification. Complete it in the open browser tab and run the command again.',
|
|
256
|
+
);
|
|
260
257
|
}
|
|
261
258
|
return downloadArticle({
|
|
262
259
|
title: data?.title || '',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
2
2
|
import { MAX_WECHAT_HTML_BYTES } from '@sovovs/bycli/download/wechat-article';
|
|
3
3
|
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
4
4
|
import { readEnvironmentCredentials, resolveBrowserCredentials } from './_wechat/auth-session.js';
|
|
@@ -107,7 +107,12 @@ export async function fetchArticleHtmlInBrowser(article, page) {
|
|
|
107
107
|
html: byteLength > maxBytes ? '' : html,
|
|
108
108
|
};
|
|
109
109
|
}, { maxBytes: MAX_WECHAT_HTML_BYTES });
|
|
110
|
-
if (result?.accessIssue)
|
|
110
|
+
if (result?.accessIssue) {
|
|
111
|
+
throw new AuthRequiredError(
|
|
112
|
+
DOMAIN,
|
|
113
|
+
'WeChat article page requires environment verification. Complete it in the open browser tab and run the command again.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
111
116
|
if (!isTrustedWechatArticleUrl(result?.finalUrl)) {
|
|
112
117
|
throw new CommandExecutionError('Article browser navigation left the trusted article path');
|
|
113
118
|
}
|
|
@@ -123,7 +128,7 @@ export async function fetchArticleHtmlInBrowser(article, page) {
|
|
|
123
128
|
}
|
|
124
129
|
return result.html;
|
|
125
130
|
} catch (error) {
|
|
126
|
-
if (error instanceof CommandExecutionError) throw error;
|
|
131
|
+
if (error instanceof CommandExecutionError || error instanceof AuthRequiredError) throw error;
|
|
127
132
|
throw new CommandExecutionError('Article browser request failed');
|
|
128
133
|
}
|
|
129
134
|
}
|
package/package.json
CHANGED
package/clis/weixin/search.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@sovovs/bycli/errors';
|
|
2
|
-
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
3
|
-
|
|
4
|
-
const SOGOU_WEIXIN_DOMAIN = 'weixin.sogou.com';
|
|
5
|
-
const DEFAULT_PAGE = 1;
|
|
6
|
-
const DEFAULT_LIMIT = 10;
|
|
7
|
-
const MAX_LIMIT = 10;
|
|
8
|
-
|
|
9
|
-
function normalizePositiveInteger(value, name, defaultValue, maxValue) {
|
|
10
|
-
if (value === undefined || value === null)
|
|
11
|
-
return defaultValue;
|
|
12
|
-
const text = String(value).trim();
|
|
13
|
-
if (!/^\d+$/.test(text)) {
|
|
14
|
-
throw new ArgumentError(`weixin search --${name} must be a positive integer`, `Pass --${name} as a whole number${maxValue ? ` from 1 to ${maxValue}` : ' greater than 0'}.`);
|
|
15
|
-
}
|
|
16
|
-
const parsed = Number(text);
|
|
17
|
-
if (!Number.isSafeInteger(parsed) || parsed < 1 || (maxValue && parsed > maxValue)) {
|
|
18
|
-
throw new ArgumentError(`weixin search --${name} is out of range`, `Pass --${name} as a whole number${maxValue ? ` from 1 to ${maxValue}` : ' greater than 0'}.`);
|
|
19
|
-
}
|
|
20
|
-
return parsed;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function normalizePage(page) {
|
|
24
|
-
return normalizePositiveInteger(page, 'page', DEFAULT_PAGE);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeLimit(limit) {
|
|
28
|
-
return normalizePositiveInteger(limit, 'limit', DEFAULT_LIMIT, MAX_LIMIT);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function buildSearchUrl(query, pageNo) {
|
|
32
|
-
const searchUrl = new URL('https://weixin.sogou.com/weixin');
|
|
33
|
-
searchUrl.searchParams.set('query', query);
|
|
34
|
-
searchUrl.searchParams.set('type', '2');
|
|
35
|
-
searchUrl.searchParams.set('page', String(pageNo));
|
|
36
|
-
searchUrl.searchParams.set('ie', 'utf8');
|
|
37
|
-
return searchUrl.toString();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildExtractSearchResultsEvaluate() {
|
|
41
|
-
return String.raw`(() => {
|
|
42
|
-
const clean = (value) => {
|
|
43
|
-
return (value || '')
|
|
44
|
-
.replace(/\s+/g, ' ')
|
|
45
|
-
.replace(/<!--red_beg-->|<!--red_end-->/g, '')
|
|
46
|
-
.replace(/document\.write\(timeConvert\('\d+'\)\)/g, '')
|
|
47
|
-
.trim();
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const absolutize = (href) => {
|
|
51
|
-
if (!href) return '';
|
|
52
|
-
try {
|
|
53
|
-
return new URL(href, window.location.origin).toString();
|
|
54
|
-
} catch {
|
|
55
|
-
return href;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const bodyText = clean(document.body && document.body.innerText);
|
|
60
|
-
const blocked = /验证码|安全验证|异常访问|访问过于频繁|请输入验证码/.test(bodyText);
|
|
61
|
-
const empty = /没有找到相关的微信文章|未找到相关|暂无相关|没有找到/.test(bodyText)
|
|
62
|
-
|| Boolean(document.querySelector('.no-result, .no_result, .s-noresult'));
|
|
63
|
-
const cards = Array.from(document.querySelectorAll('.news-list li'));
|
|
64
|
-
const extracted = cards.map((item) => {
|
|
65
|
-
const linkEl = item.querySelector('h3 a[href]');
|
|
66
|
-
const summaryEl = item.querySelector('p.txt-info');
|
|
67
|
-
const timeEl = item.querySelector('.s-p .s2');
|
|
68
|
-
return {
|
|
69
|
-
title: clean(linkEl && linkEl.textContent),
|
|
70
|
-
url: absolutize(linkEl && linkEl.getAttribute('href')),
|
|
71
|
-
summary: clean(summaryEl && summaryEl.textContent),
|
|
72
|
-
publish_time: clean(timeEl && timeEl.textContent),
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
const rows = extracted.filter((row) => row.title && row.url);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
blocked,
|
|
79
|
-
empty,
|
|
80
|
-
cardCount: cards.length,
|
|
81
|
-
invalidCount: extracted.length - rows.length,
|
|
82
|
-
rows,
|
|
83
|
-
};
|
|
84
|
-
})()`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
cli({
|
|
88
|
-
site: 'weixin',
|
|
89
|
-
name: 'search',
|
|
90
|
-
access: 'read',
|
|
91
|
-
description: '使用搜狗微信搜索公众号文章;如需导出正文 Markdown,请使用 weixin download 处理公众号文章链接',
|
|
92
|
-
domain: SOGOU_WEIXIN_DOMAIN,
|
|
93
|
-
strategy: Strategy.PUBLIC,
|
|
94
|
-
browser: true,
|
|
95
|
-
args: [
|
|
96
|
-
{ name: 'query', positional: true, required: true, help: '搜索关键词;如需正文 Markdown,请使用 weixin download 处理公众号文章链接' },
|
|
97
|
-
{ name: 'page', type: 'int', default: 1, help: '结果页码,从 1 开始' },
|
|
98
|
-
{ name: 'limit', type: 'int', default: 10, help: '返回条数,最大 10' },
|
|
99
|
-
],
|
|
100
|
-
columns: ['rank', 'page', 'title', 'url', 'summary', 'publish_time'],
|
|
101
|
-
func: async (page, kwargs) => {
|
|
102
|
-
const query = String(kwargs.query ?? '').trim();
|
|
103
|
-
if (!query) {
|
|
104
|
-
throw new ArgumentError('A search query is required.', 'Pass a non-empty keyword to search Weixin articles via Sogou.');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const pageNo = normalizePage(kwargs.page);
|
|
108
|
-
const limit = normalizeLimit(kwargs.limit);
|
|
109
|
-
const searchUrl = buildSearchUrl(query, pageNo);
|
|
110
|
-
|
|
111
|
-
let payload;
|
|
112
|
-
try {
|
|
113
|
-
await page.goto(searchUrl);
|
|
114
|
-
await page.wait(2);
|
|
115
|
-
payload = await page.evaluate(buildExtractSearchResultsEvaluate());
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
119
|
-
throw new CommandExecutionError('weixin search failed while loading Sogou results', detail);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.rows)) {
|
|
123
|
-
throw new CommandExecutionError('weixin search returned an unreadable browser payload', 'Sogou Weixin may have changed its result page structure.');
|
|
124
|
-
}
|
|
125
|
-
if (payload.blocked) {
|
|
126
|
-
throw new CommandExecutionError('Sogou Weixin blocked this search request', 'Open weixin.sogou.com in Chrome and complete any verification before retrying.');
|
|
127
|
-
}
|
|
128
|
-
if (payload.invalidCount > 0) {
|
|
129
|
-
throw new CommandExecutionError('Sogou Weixin returned article cards without required title or URL', 'The result page structure may have changed; refusing to return a partial result set.');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const rows = payload.rows;
|
|
133
|
-
if (rows.length === 0 && payload.empty) {
|
|
134
|
-
throw new EmptyResultError('weixin search', 'Try a different keyword or a different page number.');
|
|
135
|
-
}
|
|
136
|
-
if (rows.length === 0) {
|
|
137
|
-
throw new CommandExecutionError('weixin search did not expose article result cards', 'Sogou Weixin may have changed its selectors or returned a transient shell page.');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return rows.slice(0, limit).map((row, index) => ({
|
|
141
|
-
rank: (pageNo - 1) * 10 + index + 1,
|
|
142
|
-
page: pageNo,
|
|
143
|
-
title: row.title,
|
|
144
|
-
url: row.url,
|
|
145
|
-
summary: row.summary,
|
|
146
|
-
publish_time: row.publish_time,
|
|
147
|
-
}));
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
export const __test__ = {
|
|
152
|
-
MAX_LIMIT,
|
|
153
|
-
normalizePage,
|
|
154
|
-
normalizeLimit,
|
|
155
|
-
buildSearchUrl,
|
|
156
|
-
buildExtractSearchResultsEvaluate,
|
|
157
|
-
};
|