@sovovs/bycli 2.1.23 → 2.1.25

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.
@@ -1,4 +1,7 @@
1
- import { ArgumentError } from '@sovovs/bycli/errors';
1
+ import { constants } from 'node:fs';
2
+ import { access, stat } from 'node:fs/promises';
3
+ import { extname, resolve } from 'node:path';
4
+ import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
2
5
  import { cli, Strategy } from '@sovovs/bycli/registry';
3
6
  import { resolveBrowserCredentials } from './_wechat/auth-session.js';
4
7
  import { buildSecretSet, redactText } from './_wechat/redact.js';
@@ -10,9 +13,13 @@ import {
10
13
  matchPublishedRecord,
11
14
  positiveSafeInteger,
12
15
  validatePublishDate,
16
+ validatePublishedQuery,
13
17
  } from './_wechat/publish-records.js';
14
18
 
15
- const COLUMNS = ['title', 'publishedAt', 'url', 'status', 'markdownPath', 'dataPath', 'size', 'error'];
19
+ const COLUMNS = [
20
+ 'title', 'publishedAt', 'url', 'status',
21
+ 'markdownPath', 'markdownSize', 'dataPath', 'dataSize', 'error',
22
+ ];
16
23
 
17
24
  function sanitizedError(error, secrets, fallback) {
18
25
  const message = error instanceof Error ? error.message : fallback;
@@ -20,19 +27,46 @@ function sanitizedError(error, secrets, fallback) {
20
27
  .replace(/https?:\/\/mp\.weixin\.qq\.com\/\S*/giu, '[REDACTED]');
21
28
  }
22
29
 
30
+ async function validateArtifact(result, { label, expectedStatus, expectedExtension }) {
31
+ if (!result || result.status !== expectedStatus) {
32
+ throw new CommandExecutionError(`${label} returned an invalid status`);
33
+ }
34
+ if (typeof result.path !== 'string' || !result.path.trim()) {
35
+ throw new CommandExecutionError(`${label} returned no output path`);
36
+ }
37
+ if (!Number.isSafeInteger(result.size) || result.size <= 0) {
38
+ throw new CommandExecutionError(`${label} returned an invalid size`);
39
+ }
40
+ const path = resolve(result.path);
41
+ if (extname(path).toLowerCase() !== expectedExtension) {
42
+ throw new CommandExecutionError(`${label} returned an unexpected file type`);
43
+ }
44
+ let info;
45
+ try {
46
+ await access(path, constants.R_OK);
47
+ info = await stat(path);
48
+ } catch {
49
+ throw new CommandExecutionError(`${label} returned an unreadable file`);
50
+ }
51
+ if (!info.isFile() || info.size <= 0 || info.size !== result.size) {
52
+ throw new CommandExecutionError(`${label} returned an unreadable or mismatched file`);
53
+ }
54
+ return { ...result, path, size: info.size };
55
+ }
56
+
23
57
  export const downloadPublishDataCommand = cli({
24
58
  site: 'weixin',
25
59
  name: 'download-publish-data',
26
60
  access: 'write',
27
61
  domain: 'mp.weixin.qq.com',
28
- description: 'Match a Weixin published article and save its content analysis as Markdown',
62
+ description: 'Match a Weixin published article and save its Excel data and Markdown analysis',
29
63
  strategy: Strategy.INTERCEPT,
30
64
  browser: true,
31
65
  navigateBefore: false,
32
66
  args: [
33
67
  { name: 'query', positional: true, required: true, help: 'Exact article URL or title text' },
34
68
  { name: 'date', help: 'Optional publication date in YYYY-MM-DD' },
35
- { name: 'output', default: './weixin-publish-data', help: 'Directory for generated Markdown reports' },
69
+ { name: 'output', default: './weixin-publish-data', help: 'Directory for generated Excel data and Markdown reports' },
36
70
  { name: 'max-pages', type: 'int', default: 5, help: 'Maximum published-record pages to scan' },
37
71
  { name: 'timeout', type: 'int', default: 60, help: 'Maximum seconds for page capture' },
38
72
  ],
@@ -40,6 +74,7 @@ export const downloadPublishDataCommand = cli({
40
74
  func: async (page, args) => {
41
75
  const query = String(args.query ?? '').trim();
42
76
  if (!query) throw new ArgumentError('query required');
77
+ const validatedQuery = validatePublishedQuery(query);
43
78
 
44
79
  const timeoutSeconds = positiveSafeInteger(args.timeout, 'timeout', 60);
45
80
  const maxPages = positiveSafeInteger(args['max-pages'], 'max-pages', 5);
@@ -53,7 +88,7 @@ export const downloadPublishDataCommand = cli({
53
88
  maxPages,
54
89
  timeout: timeoutSeconds,
55
90
  });
56
- const record = matchPublishedRecord(rows, query, validatedDate);
91
+ const record = matchPublishedRecord(rows, validatedQuery, validatedDate);
57
92
  const detailUrl = buildDetailUrl(record, token);
58
93
  const outputDir = args.output ?? './weixin-publish-data';
59
94
  const commonOptions = {
@@ -64,31 +99,45 @@ export const downloadPublishDataCommand = cli({
64
99
  };
65
100
  const secrets = buildSecretSet({ token, cookie });
66
101
 
102
+ let dataResult = null;
103
+ let markdownResult = null;
104
+ const errors = [];
67
105
  try {
68
106
  const result = await downloadPublishData(page, commonOptions);
69
- return [{ title: record.title, publishedAt: record.publishedAt, url: record.url,
70
- status: 'saved', markdownPath: null, dataPath: result.path,
71
- size: result.size, error: null }];
72
- } catch (downloadError) {
73
- const downloadMessage = sanitizedError(downloadError, secrets, 'Excel download failed');
74
- try {
75
- const result = await collectPublishAnalysis(page, {
76
- ...commonOptions,
77
- publishedAt: record.publishedAt,
78
- });
79
- return [{ title: record.title, publishedAt: record.publishedAt, url: record.url,
80
- status: 'saved', markdownPath: result.path, dataPath: null,
81
- size: result.size, error: downloadMessage }];
82
- } catch (analysisError) {
83
- const analysisMessage = sanitizedError(
84
- analysisError,
85
- secrets,
86
- 'Markdown fallback failed',
87
- );
88
- return [{ title: record.title, publishedAt: record.publishedAt, url: record.url,
89
- status: 'failed', markdownPath: null, dataPath: null, size: null,
90
- error: `Excel download failed: ${downloadMessage}; Markdown fallback failed: ${analysisMessage}` }];
91
- }
107
+ dataResult = await validateArtifact(result, {
108
+ label: 'Excel artifact',
109
+ expectedStatus: 'downloaded',
110
+ expectedExtension: '.xls',
111
+ });
112
+ } catch (error) {
113
+ errors.push(`Excel download failed: ${sanitizedError(error, secrets, 'Excel download failed')}`);
114
+ }
115
+ try {
116
+ const result = await collectPublishAnalysis(page, {
117
+ ...commonOptions,
118
+ publishedAt: record.publishedAt,
119
+ });
120
+ markdownResult = await validateArtifact(result, {
121
+ label: 'Markdown artifact',
122
+ expectedStatus: 'saved',
123
+ expectedExtension: '.md',
124
+ });
125
+ } catch (error) {
126
+ errors.push(`Markdown analysis failed: ${sanitizedError(error, secrets, 'Markdown analysis failed')}`);
92
127
  }
128
+
129
+ const status = dataResult && markdownResult ? 'downloaded'
130
+ : dataResult || markdownResult ? 'partial' : 'failed';
131
+ return [{
132
+ title: record.title,
133
+ publishedAt: record.publishedAt,
134
+ url: record.url,
135
+ status,
136
+ markdownPath: markdownResult?.path ?? null,
137
+ markdownSize: markdownResult?.size ?? null,
138
+ dataPath: dataResult?.path ?? null,
139
+ dataSize: dataResult?.size ?? null,
140
+ error: errors.length > 0 ? errors.join('; ') : null,
141
+ }];
93
142
  },
94
143
  });
@@ -10,45 +10,19 @@ import { cli, Strategy } from '@sovovs/bycli/registry';
10
10
  import { downloadArticle } from '@sovovs/bycli/download/article-download';
11
11
  import { AuthRequiredError } from '@sovovs/bycli/errors';
12
12
  import { buildExtractWechatArticleContentJs } from './_wechat/article-content.js';
13
+ import {
14
+ isTrustedSogouRedirectUrl,
15
+ isTrustedWechatArticleUrl,
16
+ normalizeWechatUrl,
17
+ resolveWechatArticleUrl,
18
+ } from './_wechat/article-link.js';
13
19
  export { extractWechatArticleContent } from './_wechat/article-content.js';
14
- // ============================================================
15
- // URL Normalization
16
- // ============================================================
17
- /**
18
- * Normalize a pasted WeChat article URL.
19
- */
20
- export function normalizeWechatUrl(raw) {
21
- let s = (raw || '').trim();
22
- if (!s)
23
- return s;
24
- // Strip wrapping quotes / angle brackets
25
- if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
26
- s = s.slice(1, -1).trim();
27
- }
28
- if (s.startsWith('<') && s.endsWith('>')) {
29
- s = s.slice(1, -1).trim();
30
- }
31
- // Remove backslash escapes before URL-significant characters
32
- s = s.replace(/\\+([:/&?=#%])/g, '$1');
33
- // Decode HTML entities
34
- s = s.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
35
- // Allow bare hostnames
36
- if (s.startsWith('mp.weixin.qq.com/') || s.startsWith('//mp.weixin.qq.com/')) {
37
- s = 'https://' + s.replace(/^\/+/, '');
38
- }
39
- // Force https for mp.weixin.qq.com
40
- try {
41
- const parsed = new URL(s);
42
- if (['http:', 'https:'].includes(parsed.protocol) && parsed.hostname.toLowerCase() === 'mp.weixin.qq.com') {
43
- parsed.protocol = 'https:';
44
- s = parsed.toString();
45
- }
46
- }
47
- catch {
48
- // Ignore parse errors
49
- }
50
- return s;
51
- }
20
+ export {
21
+ isTrustedSogouRedirectUrl,
22
+ isTrustedWechatArticleUrl,
23
+ normalizeWechatUrl,
24
+ resolveWechatArticleUrl as resolveWechatDownloadUrl,
25
+ };
52
26
  /**
53
27
  * Format a WeChat article timestamp as a UTC+8 datetime string.
54
28
  * Accepts either Unix seconds or milliseconds.
@@ -183,15 +157,12 @@ cli({
183
157
  { name: 'output', default: './weixin-articles', help: 'Output directory' },
184
158
  { name: 'download-images', type: 'boolean', default: true, help: 'Download images locally' },
185
159
  ],
186
- columns: ['title', 'author', 'publish_time', 'status', 'size', 'saved'],
160
+ columns: ['title', 'author', 'publish_time', 'status', 'size', 'saved', 'source_url', 'resolved_url'],
187
161
  func: async (page, kwargs) => {
188
- const rawUrl = kwargs.url;
189
- const url = normalizeWechatUrl(rawUrl);
190
- if (!url.startsWith('https://mp.weixin.qq.com/')) {
191
- return [{ title: 'Error', author: '-', publish_time: '-', status: 'invalid URL', size: '-', saved: '-' }];
192
- }
193
- // Navigate and wait for content to load
194
- await page.goto(url);
162
+ const { sourceUrl, resolvedUrl, alreadyNavigated } = await resolveWechatArticleUrl(page, kwargs.url);
163
+ // Navigate and wait for content to load. Sogou resolution already lands on the article.
164
+ if (!alreadyNavigated)
165
+ await page.goto(resolvedUrl);
195
166
  await page.wait(5);
196
167
  // Extract article data in browser context
197
168
  const data = await page.evaluate(`
@@ -255,11 +226,11 @@ cli({
255
226
  'WeChat article page requires environment verification. Complete it in the open browser tab and run the command again.',
256
227
  );
257
228
  }
258
- return downloadArticle({
229
+ const rows = await downloadArticle({
259
230
  title: data?.title || '',
260
231
  author: data?.author,
261
232
  publishTime: data?.publishTime,
262
- sourceUrl: url,
233
+ sourceUrl: resolvedUrl,
263
234
  contentHtml: data?.contentHtml || '',
264
235
  codeBlocks: data?.codeBlocks,
265
236
  imageUrls: data?.imageUrls,
@@ -274,5 +245,6 @@ cli({
274
245
  },
275
246
  secureMarkdown: true,
276
247
  });
248
+ return rows.map(row => ({ ...row, source_url: sourceUrl, resolved_url: resolvedUrl }));
277
249
  },
278
250
  });
@@ -1,12 +1,21 @@
1
- import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
1
+ import {
2
+ ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError,
3
+ } from '@sovovs/bycli/errors';
2
4
  import { MAX_WECHAT_HTML_BYTES } from '@sovovs/bycli/download/wechat-article';
3
5
  import { cli, Strategy } from '@sovovs/bycli/registry';
4
6
  import { readEnvironmentCredentials, resolveBrowserCredentials } from './_wechat/auth-session.js';
5
7
  import {
6
- callCrawler, collectArticles, createWechatApi, isTrustedWechatArticleUrl, saveArticles,
8
+ combineArticleFallbackErrors,
9
+ isEligibleArticleFallbackError,
10
+ withMissingFallbackName,
11
+ } from './_wechat/article-fallback-policy.js';
12
+ import { createArticleIndexFetcher } from './_wechat/article-index.js';
13
+ import {
14
+ callCrawler, collectArticles, isTrustedWechatArticleUrl, saveArticles,
7
15
  } from './_wechat/crawler-runtime.js';
8
16
  import { readAuthSource } from './_wechat/args.js';
9
17
  import { wechatArticleToMarkdown } from './_wechat/markdown.js';
18
+ import { collectSogouAccountArticles } from './_wechat/sogou-fallback.js';
10
19
 
11
20
  const DOMAIN = 'mp.weixin.qq.com';
12
21
  const browserRequired = args => readAuthSource(args) === 'browser';
@@ -155,11 +164,11 @@ export const saveArticlesCommand = cli({
155
164
  description: 'Download WeChat official-account articles as Markdown files',
156
165
  strategy: Strategy.COOKIE, browser: browserRequired,
157
166
  args: [
158
- { name: 'fakeid', positional: true, required: true, help: 'Official-account fakeid returned by weixin accounts' }, { name: 'name', help: 'Official-account name used in Markdown metadata' },
167
+ { name: 'fakeid', positional: true, required: true, help: 'Official-account fakeid returned by weixin accounts' }, { name: 'name', help: 'Official-account name; exact case-insensitive match required for browser Sogou fallback' },
159
168
  { name: 'output', default: './weixin-articles', help: 'Directory for saved Markdown files' }, { name: 'limit', type: 'int', help: 'Maximum number of articles to save' },
160
169
  { name: 'max-pages', type: 'int', help: 'Maximum number of history pages to scan' }, { name: 'auth-source', default: 'browser', choices: ['browser', 'env'], help: 'Credential source: browser session or environment variables' },
161
170
  ],
162
- columns: ['title', 'status', 'stage', 'path', 'error', 'url'],
171
+ columns: ['title', 'status', 'stage', 'path', 'error', 'url', 'source', 'coverage'],
163
172
  func: async (page, args) => {
164
173
  const fakeid = String(args.fakeid ?? '').trim();
165
174
  if (!fakeid) throw new ArgumentError('fakeid is required');
@@ -167,21 +176,56 @@ export const saveArticlesCommand = cli({
167
176
  const credentials = authSource === 'env'
168
177
  ? readEnvironmentCredentials(false) : await resolveBrowserCredentials(page);
169
178
  const articleHtmlDownloader = createArticleHtmlDownloader({ authSource, page });
170
- const rows = await callCrawler(async () => {
171
- const { fetchPage } = createWechatApi(credentials);
172
- const { articles } = await collectArticles({ fakeid, fetchPage, limit: args.limit, maxPages: args['max-pages'] });
173
- return saveArticles({
179
+ const fetchPage = createArticleIndexFetcher({ page, source: authSource, credentials });
180
+ let articles;
181
+ let resolutionFailures = [];
182
+ let source = 'wechat';
183
+ let coverage = null;
184
+ try {
185
+ const result = await callCrawler(() => collectArticles({
186
+ fakeid, fetchPage, limit: args.limit, maxPages: args['max-pages'],
187
+ }));
188
+ articles = result.articles;
189
+ if (articles.length === 0) {
190
+ throw new EmptyResultError('weixin save-articles', `No published articles were found for ${fakeid}.`);
191
+ }
192
+ } catch (primaryError) {
193
+ if (authSource !== 'browser' || !isEligibleArticleFallbackError(primaryError)) throw primaryError;
194
+ const accountName = String(args.name ?? '').trim();
195
+ if (!accountName) throw withMissingFallbackName('weixin save-articles', primaryError);
196
+ try {
197
+ const fallback = await collectSogouAccountArticles({
198
+ page, accountName, limit: args.limit, maxPages: args['max-pages'],
199
+ resolutionPolicy: 'rows',
200
+ });
201
+ articles = fallback.articles;
202
+ resolutionFailures = fallback.resolutionFailures;
203
+ source = fallback.source;
204
+ coverage = fallback.coverage;
205
+ } catch (fallbackError) {
206
+ throw combineArticleFallbackErrors({
207
+ operation: 'weixin save-articles', primaryError, fallbackError, credentials,
208
+ });
209
+ }
210
+ }
211
+
212
+ const savedRows = articles.length === 0 ? [] : await callCrawler(() => saveArticles({
174
213
  articles, accountName: String(args.name ?? '').trim(),
175
214
  outputDir: args.output ?? './weixin-articles', fetchArticleHtml: articleHtmlDownloader,
176
215
  buildMarkdown: (article, html) => wechatArticleToMarkdown({
177
216
  html, title: article.title, accountName: String(args.name ?? '').trim(), author: article.author,
178
217
  publishedAt: article.publishedAt, digest: article.digest, url: article.url,
179
218
  }), existingFilePolicy: 'suffix',
180
- });
181
- });
182
- return rows.map(row => ({
219
+ }));
220
+ const orderedRows = source === 'sogou'
221
+ ? [
222
+ ...savedRows.map((row, index) => ({ ...row, order: articles[index]?.order ?? index })),
223
+ ...resolutionFailures,
224
+ ].sort((left, right) => left.order - right.order)
225
+ : savedRows;
226
+ return orderedRows.map(row => ({
183
227
  title: row.title, status: row.status, stage: row.stage || null, path: row.saved || null,
184
- error: row.error || null, url: row.url,
228
+ error: row.error || null, url: row.url, source, coverage,
185
229
  }));
186
230
  },
187
231
  });
@@ -1,30 +1,15 @@
1
- import { ArgumentError, CommandExecutionError, EmptyResultError } from '@sovovs/bycli/errors';
1
+ import { EmptyResultError } from '@sovovs/bycli/errors';
2
2
  import { cli, Strategy } from '@sovovs/bycli/registry';
3
+ import {
4
+ normalizePositiveInteger,
5
+ searchSogouArticlePage,
6
+ } from './_wechat/sogou-search.js';
3
7
 
4
8
  const SOGOU_WEIXIN_DOMAIN = 'weixin.sogou.com';
5
9
  const DEFAULT_PAGE = 1;
6
10
  const DEFAULT_LIMIT = 10;
7
11
  const MAX_LIMIT = 10;
8
12
 
9
- function normalizePositiveInteger(value, name, defaultValue, maxValue) {
10
- if (value === undefined || value === null) return defaultValue;
11
- const text = String(value).trim();
12
- if (!/^\d+$/.test(text)) {
13
- throw new ArgumentError(
14
- `weixin sougousearch --${name} must be a positive integer`,
15
- `Pass --${name} as a whole number${maxValue ? ` from 1 to ${maxValue}` : ' greater than 0'}.`,
16
- );
17
- }
18
- const parsed = Number(text);
19
- if (!Number.isSafeInteger(parsed) || parsed < 1 || (maxValue && parsed > maxValue)) {
20
- throw new ArgumentError(
21
- `weixin sougousearch --${name} is out of range`,
22
- `Pass --${name} as a whole number${maxValue ? ` from 1 to ${maxValue}` : ' greater than 0'}.`,
23
- );
24
- }
25
- return parsed;
26
- }
27
-
28
13
  function normalizePage(page) {
29
14
  return normalizePositiveInteger(page, 'page', DEFAULT_PAGE);
30
15
  }
@@ -33,63 +18,6 @@ function normalizeLimit(limit) {
33
18
  return normalizePositiveInteger(limit, 'limit', DEFAULT_LIMIT, MAX_LIMIT);
34
19
  }
35
20
 
36
- function buildSearchUrl(query, pageNo) {
37
- const searchUrl = new URL('https://weixin.sogou.com/weixin');
38
- searchUrl.searchParams.set('query', query);
39
- searchUrl.searchParams.set('type', '2');
40
- searchUrl.searchParams.set('page', String(pageNo));
41
- searchUrl.searchParams.set('ie', 'utf8');
42
- return searchUrl.toString();
43
- }
44
-
45
- function buildExtractSearchResultsEvaluate() {
46
- return String.raw`(() => {
47
- const clean = (value) => {
48
- return (value || '')
49
- .replace(/\s+/g, ' ')
50
- .replace(/<!--red_beg-->|<!--red_end-->/g, '')
51
- .replace(/document\.write\(timeConvert\('\d+'\)\)/g, '')
52
- .trim();
53
- };
54
-
55
- const absolutize = (href) => {
56
- if (!href) return '';
57
- try {
58
- return new URL(href, window.location.origin).toString();
59
- } catch {
60
- return href;
61
- }
62
- };
63
-
64
- const bodyText = clean(document.body && document.body.innerText);
65
- const blocked = /验证码|安全验证|异常访问|访问过于频繁|请输入验证码/.test(bodyText);
66
- const empty = /没有找到相关的微信文章|未找到相关|暂无相关|没有找到/.test(bodyText)
67
- || Boolean(document.querySelector('.no-result, .no_result, .s-noresult'));
68
- const cards = Array.from(document.querySelectorAll('.news-list li'));
69
- const extracted = cards.map((item) => {
70
- const linkEl = item.querySelector('h3 a[href]');
71
- const summaryEl = item.querySelector('p.txt-info');
72
- const accountEl = item.querySelector('.s-p .all-time-y2');
73
- const timeEl = item.querySelector('.s-p .s2');
74
- return {
75
- title: clean(linkEl && linkEl.textContent),
76
- account: clean(accountEl && accountEl.textContent),
77
- url: absolutize(linkEl && linkEl.getAttribute('href')),
78
- summary: clean(summaryEl && summaryEl.textContent),
79
- publish_time: clean(timeEl && timeEl.textContent),
80
- };
81
- });
82
- const rows = extracted.filter((row) => row.title && row.url);
83
-
84
- return {
85
- blocked,
86
- empty,
87
- invalidCount: extracted.length - rows.length,
88
- rows,
89
- };
90
- })()`;
91
- }
92
-
93
21
  export const weixinSearchCommand = cli({
94
22
  site: 'weixin',
95
23
  name: 'sougousearch',
@@ -106,50 +34,20 @@ export const weixinSearchCommand = cli({
106
34
  columns: ['rank', 'page', 'title', 'account', 'url', 'summary', 'publish_time'],
107
35
  func: async (page, kwargs) => {
108
36
  const query = String(kwargs.query ?? '').trim();
109
- if (!query) {
110
- throw new ArgumentError('A search query is required.', 'Pass a non-empty keyword to search Weixin articles via Sogou.');
111
- }
112
-
113
37
  const pageNo = normalizePage(kwargs.page);
114
38
  const limit = normalizeLimit(kwargs.limit);
115
- const searchUrl = buildSearchUrl(query, pageNo);
116
-
117
- let payload;
118
- try {
119
- await page.goto(searchUrl);
120
- await page.wait(2);
121
- payload = await page.evaluate(buildExtractSearchResultsEvaluate());
122
- } catch (error) {
123
- const detail = error instanceof Error ? error.message : String(error);
124
- throw new CommandExecutionError('weixin sougousearch failed while loading Sogou results', detail);
125
- }
126
-
127
- if (!payload || typeof payload !== 'object' || !Array.isArray(payload.rows)) {
128
- throw new CommandExecutionError('weixin sougousearch returned an unreadable browser payload', 'Sogou Weixin may have changed its result page structure.');
129
- }
130
- if (payload.blocked) {
131
- throw new CommandExecutionError('Sogou Weixin blocked this search request', 'Open weixin.sogou.com in Chrome and complete any verification before retrying.');
132
- }
133
- if (payload.invalidCount > 0) {
134
- 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.');
135
- }
136
-
137
- const rows = payload.rows;
138
- if (rows.length === 0 && payload.empty) {
39
+ const result = await searchSogouArticlePage(page, { query, pageNo });
40
+ if (result.state === 'empty') {
139
41
  throw new EmptyResultError('weixin sougousearch', 'Try a different keyword or a different page number.');
140
42
  }
141
- if (rows.length === 0) {
142
- throw new CommandExecutionError('weixin sougousearch did not expose article result cards', 'Sogou Weixin may have changed its selectors or returned a transient shell page.');
143
- }
144
-
145
- return rows.slice(0, limit).map((row, index) => ({
43
+ return result.rows.slice(0, limit).map((row, index) => ({
146
44
  rank: (pageNo - 1) * 10 + index + 1,
147
45
  page: pageNo,
148
46
  title: row.title,
149
47
  account: row.account,
150
48
  url: row.url,
151
49
  summary: row.summary,
152
- publish_time: row.publish_time,
50
+ publish_time: row.publishTime,
153
51
  }));
154
52
  },
155
53
  });
@@ -158,6 +56,4 @@ export const __test__ = {
158
56
  MAX_LIMIT,
159
57
  normalizePage,
160
58
  normalizeLimit,
161
- buildSearchUrl,
162
- buildExtractSearchResultsEvaluate,
163
59
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.23",
3
+ "version": "2.1.25",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },