@sovovs/bycli 2.1.53 → 2.1.55

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
@@ -28753,51 +28753,6 @@
28753
28753
  "sourceFile": "weibo/user-posts.js",
28754
28754
  "navigateBefore": "https://weibo.com"
28755
28755
  },
28756
- {
28757
- "site": "weixin",
28758
- "name": "accounts",
28759
- "description": "Search WeChat official accounts and return their fakeids",
28760
- "access": "read",
28761
- "domain": "mp.weixin.qq.com",
28762
- "strategy": "intercept",
28763
- "browser": "conditional",
28764
- "args": [
28765
- {
28766
- "name": "query",
28767
- "type": "str",
28768
- "required": true,
28769
- "positional": true,
28770
- "help": "Official-account name to search for"
28771
- },
28772
- {
28773
- "name": "limit",
28774
- "type": "int",
28775
- "default": 10,
28776
- "required": false,
28777
- "help": "Maximum number of matching accounts to return"
28778
- },
28779
- {
28780
- "name": "auth-source",
28781
- "type": "str",
28782
- "default": "browser",
28783
- "required": false,
28784
- "help": "Credential source: browser session or environment variables",
28785
- "choices": [
28786
- "browser",
28787
- "env"
28788
- ]
28789
- }
28790
- ],
28791
- "columns": [
28792
- "nickname",
28793
- "fakeid",
28794
- "alias"
28795
- ],
28796
- "type": "js",
28797
- "modulePath": "weixin/accounts.js",
28798
- "sourceFile": "weixin/accounts.js",
28799
- "navigateBefore": true
28800
- },
28801
28756
  {
28802
28757
  "site": "weixin",
28803
28758
  "name": "article-fetch",
@@ -28909,7 +28864,7 @@
28909
28864
  "type": "str",
28910
28865
  "required": true,
28911
28866
  "positional": true,
28912
- "help": "Official-account fakeid returned by weixin accounts"
28867
+ "help": "Official-account fakeid returned by weixin get-public-account-info"
28913
28868
  },
28914
28869
  {
28915
28870
  "name": "name",
@@ -29514,6 +29469,51 @@
29514
29469
  "modulePath": "weixin/freepublish-list.js",
29515
29470
  "sourceFile": "weixin/freepublish-list.js"
29516
29471
  },
29472
+ {
29473
+ "site": "weixin",
29474
+ "name": "get-public-account-info",
29475
+ "description": "Search WeChat official accounts and return their fakeids",
29476
+ "access": "read",
29477
+ "domain": "mp.weixin.qq.com",
29478
+ "strategy": "intercept",
29479
+ "browser": "conditional",
29480
+ "args": [
29481
+ {
29482
+ "name": "query",
29483
+ "type": "str",
29484
+ "required": true,
29485
+ "positional": true,
29486
+ "help": "Official-account name to search for"
29487
+ },
29488
+ {
29489
+ "name": "limit",
29490
+ "type": "int",
29491
+ "default": 10,
29492
+ "required": false,
29493
+ "help": "Maximum number of matching accounts to return"
29494
+ },
29495
+ {
29496
+ "name": "auth-source",
29497
+ "type": "str",
29498
+ "default": "browser",
29499
+ "required": false,
29500
+ "help": "Credential source: browser session or environment variables",
29501
+ "choices": [
29502
+ "browser",
29503
+ "env"
29504
+ ]
29505
+ }
29506
+ ],
29507
+ "columns": [
29508
+ "nickname",
29509
+ "fakeid",
29510
+ "alias"
29511
+ ],
29512
+ "type": "js",
29513
+ "modulePath": "weixin/get-public-account-info.js",
29514
+ "sourceFile": "weixin/get-public-account-info.js",
29515
+ "navigateBefore": true
29516
+ },
29517
29517
  {
29518
29518
  "site": "weixin",
29519
29519
  "name": "home-overview",
@@ -29765,7 +29765,7 @@
29765
29765
  "type": "str",
29766
29766
  "required": true,
29767
29767
  "positional": true,
29768
- "help": "Official-account fakeid returned by weixin accounts"
29768
+ "help": "Official-account fakeid returned by weixin get-public-account-info"
29769
29769
  },
29770
29770
  {
29771
29771
  "name": "name",
@@ -204,6 +204,12 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
204
204
  }
205
205
  return cleanValueText(node);
206
206
  };
207
+ const textWithout = (node, selector) => {
208
+ if (!node) return null;
209
+ const copy = node.cloneNode(true);
210
+ copy.querySelectorAll(selector).forEach(item => item.remove());
211
+ return compact(copy.textContent);
212
+ };
207
213
  const switchValue = row => {
208
214
  const node = firstVisible(row, [
209
215
  'input[type="checkbox"]',
@@ -263,6 +269,45 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
263
269
  const label = compact(heading && heading.textContent)
264
270
  || compact(sectionNode.getAttribute('aria-label'))
265
271
  || '其他信息';
272
+ const authorizationTable = Array.from(sectionNode.querySelectorAll('table'))
273
+ .filter(visible)
274
+ .find(table => {
275
+ const headers = Array.from(table.querySelectorAll('thead th, tr > th'))
276
+ .filter(visible)
277
+ .map(node => compact(node.textContent));
278
+ return headers.includes('第三方平台名称')
279
+ && headers.includes('已授权权限')
280
+ && headers.includes('授权时间');
281
+ }) || null;
282
+ const records = authorizationTable === null ? null : Array.from(
283
+ authorizationTable.querySelectorAll('tbody > tr, tr'),
284
+ ).filter(visible).flatMap(row => {
285
+ const cells = Array.from(row.querySelectorAll(':scope > td')).filter(visible);
286
+ if (cells.length < 3 || row.querySelector('.empty_tips')) return [];
287
+ const nameNode = firstVisible(cells[0], [
288
+ '.plugin_info h4',
289
+ '.plugin_info .name',
290
+ 'h4',
291
+ ]);
292
+ const name = compact(nameNode && nameNode.textContent);
293
+ if (!name) return [];
294
+ const descriptionNode = firstVisible(cells[0], [
295
+ '.plugin_info .desc',
296
+ '.plugin_info p',
297
+ ]);
298
+ const permissions = Array.from(cells[1].querySelectorAll('.privilege'))
299
+ .filter(visible)
300
+ .flatMap(node => {
301
+ const permission = textWithout(node, '.dot');
302
+ return permission ? [permission] : [];
303
+ });
304
+ return [{
305
+ name,
306
+ description: compact(descriptionNode && descriptionNode.textContent),
307
+ permissions,
308
+ authorized_at: cleanValueText(cells[2]),
309
+ }];
310
+ });
266
311
  let rows = Array.from(sectionNode.querySelectorAll(rowSelector)).filter(visible);
267
312
  rows = rows.filter(node => !rows.some(other => other !== node && other.contains(node)));
268
313
  const fields = [];
@@ -339,7 +384,10 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
339
384
  }
340
385
  }
341
386
 
342
- if (fields.length > 0) return [{ label, fields }];
387
+ if (fields.length > 0 || (records && records.length > 0)) {
388
+ return [{ label, fields, ...(records === null ? {} : { records }) }];
389
+ }
390
+ if (records !== null) return [{ label, fields, records, empty: true }];
343
391
  const empty = Boolean(sectionNode.querySelector('table thead th, table tr > th'));
344
392
  return empty ? [{ label, fields, empty: true }] : [];
345
393
  });
@@ -361,6 +409,25 @@ function normalizeField(field, tabLabel, sectionIndex, fieldIndex) {
361
409
  };
362
410
  }
363
411
 
412
+ function normalizeAuthorizationRecord(record, tabLabel, sectionIndex, recordIndex) {
413
+ const prefix = `WeChat ${tabLabel} returned an invalid authorization record at section ${sectionIndex} index ${recordIndex}`;
414
+ execution(object(record), prefix);
415
+ const name = text(record.name);
416
+ const authorizedAt = text(record.authorized_at);
417
+ execution(name && authorizedAt && Array.isArray(record.permissions), prefix);
418
+ const permissions = record.permissions.map((permission, permissionIndex) => {
419
+ const normalized = text(permission);
420
+ execution(normalized, `${prefix} permission ${permissionIndex}`);
421
+ return normalized;
422
+ });
423
+ return {
424
+ name,
425
+ description: text(record.description),
426
+ permissions,
427
+ authorized_at: authorizedAt,
428
+ };
429
+ }
430
+
364
431
  export function normalizeUserInfoTab(tabId, payload) {
365
432
  const definition = TAB_DEFINITIONS.find(tab => tab.id === tabId);
366
433
  execution(definition, `WeChat account settings returned an unknown tab: ${String(tabId)}`);
@@ -381,8 +448,14 @@ export function normalizeUserInfoTab(tabId, payload) {
381
448
  const fields = rawFields.map((field, fieldIndex) => (
382
449
  normalizeField(field, definition.label, sectionIndex, fieldIndex)
383
450
  ));
384
- return fields.length > 0 || section.empty === true
385
- ? [{ label, fields }]
451
+ const hasRecords = Object.prototype.hasOwnProperty.call(section, 'records');
452
+ const rawRecords = hasRecords ? section.records : [];
453
+ execution(Array.isArray(rawRecords), prefix);
454
+ const records = rawRecords.map((record, recordIndex) => (
455
+ normalizeAuthorizationRecord(record, definition.label, sectionIndex, recordIndex)
456
+ ));
457
+ return fields.length > 0 || records.length > 0 || section.empty === true
458
+ ? [{ label, fields, ...(hasRecords ? { records } : {}) }]
386
459
  : [];
387
460
  });
388
461
 
@@ -19,7 +19,7 @@ export const articlesCommand = cli({
19
19
  description: 'List published articles from a WeChat official account',
20
20
  strategy: Strategy.COOKIE, browser: browserRequired,
21
21
  args: [
22
- { name: 'fakeid', positional: true, required: true, help: 'Official-account fakeid returned by weixin accounts' },
22
+ { name: 'fakeid', positional: true, required: true, help: 'Official-account fakeid returned by weixin get-public-account-info' },
23
23
  { name: 'name', help: 'Official-account name; exact case-insensitive match required for browser Sogou fallback' }, { name: 'limit', type: 'int', help: 'Maximum number of articles to return' }, { name: 'max-pages', type: 'int', help: 'Maximum number of history pages to scan' },
24
24
  { name: 'auth-source', default: 'browser', choices: ['browser', 'env'], help: 'Credential source: browser session or environment variables' },
25
25
  ],
@@ -8,8 +8,8 @@ import { readAuthSource } from './_wechat/args.js';
8
8
  const DOMAIN = 'mp.weixin.qq.com';
9
9
  const browserRequired = args => readAuthSource(args) === 'browser';
10
10
 
11
- export const accountsCommand = cli({
12
- site: 'weixin', name: 'accounts', access: 'read', domain: DOMAIN,
11
+ export const getPublicAccountInfoCommand = cli({
12
+ site: 'weixin', name: 'get-public-account-info', access: 'read', domain: DOMAIN,
13
13
  description: 'Search WeChat official accounts and return their fakeids',
14
14
  strategy: Strategy.INTERCEPT, browser: browserRequired,
15
15
  args: [
@@ -32,7 +32,7 @@ export const accountsCommand = cli({
32
32
  credentials = { ...credentials, fingerprint: await captureSearchBizFingerprint(page, query) };
33
33
  }
34
34
  const rows = await executeSearchBiz({ page, source: authSource, credentials, query, limit });
35
- if (rows.length === 0) throw new EmptyResultError('weixin accounts', `No official accounts matched "${query}".`);
35
+ if (rows.length === 0) throw new EmptyResultError('weixin get-public-account-info', `No official accounts matched "${query}".`);
36
36
  return rows.map(row => ({ nickname: row.nickname, fakeid: row.fakeid, alias: row.alias || null }));
37
37
  },
38
38
  });
@@ -17,6 +17,12 @@ function sourceValue(value) {
17
17
  return source;
18
18
  }
19
19
 
20
+ function limitValue(value) {
21
+ const limit = value ?? 20;
22
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new ArgumentError('limit must be a positive safe integer');
23
+ return limit;
24
+ }
25
+
20
26
  export function projectOfficialRows(rows, fallbackReason = null) {
21
27
  return rows.map(row => ({
22
28
  article_id: row.article_id, title: row.title, author: row.author, digest: row.digest,
@@ -39,6 +45,29 @@ async function browserRows(page, args, fallbackReason) {
39
45
  }));
40
46
  }
41
47
 
48
+ async function officialRows(args) {
49
+ const limit = limitValue(args.limit);
50
+ const rows = [];
51
+ let offset = 0;
52
+ while (rows.length < limit) {
53
+ const remaining = limit - rows.length;
54
+ const count = remaining > 20 ? 20 : remaining;
55
+ let pageRows;
56
+ try {
57
+ pageRows = await freepublishListCommand.func({
58
+ ...args, offset, count, content: args.content ?? 'none',
59
+ });
60
+ } catch (error) {
61
+ if (error instanceof EmptyResultError && rows.length > 0) break;
62
+ throw error;
63
+ }
64
+ rows.push(...pageRows);
65
+ offset += count;
66
+ if (pageRows.length < count) break;
67
+ }
68
+ return rows.slice(0, limit);
69
+ }
70
+
42
71
  export const publishedArticlesCommand = cli({
43
72
  site: 'weixin', name: 'published-articles', access: 'read', domain: 'mp.weixin.qq.com',
44
73
  description: 'List published Weixin articles with optional official-API-to-browser fallback',
@@ -56,17 +85,17 @@ export const publishedArticlesCommand = cli({
56
85
  columns: FACADE_COLUMNS,
57
86
  func: async (page, args) => {
58
87
  const source = sourceValue(args.source);
59
- if (source === 'browser') return browserRows(page, args, null);
88
+ const limit = limitValue(args.limit);
89
+ const normalizedArgs = { ...args, limit };
90
+ if (source === 'browser') return browserRows(page, normalizedArgs, null);
60
91
  const credentials = readOfficialApiCredentials(args);
61
- if (source === 'auto' && !credentials.configured) return browserRows(page, args, 'api-not-configured');
92
+ if (source === 'auto' && !credentials.configured) return browserRows(page, normalizedArgs, 'api-not-configured');
62
93
  try {
63
- const rows = await freepublishListCommand.func({
64
- ...args, offset: 0, count: Math.min(args.limit ?? 20, 20), content: args.content ?? 'none',
65
- });
94
+ const rows = await officialRows(normalizedArgs);
66
95
  return projectOfficialRows(rows);
67
96
  } catch (error) {
68
97
  if (source === 'auto' && isFreepublishFallbackEligible(error)) {
69
- return browserRows(page, args, 'api-not-authorized');
98
+ return browserRows(page, normalizedArgs, 'api-not-authorized');
70
99
  }
71
100
  if (error instanceof EmptyResultError) throw error;
72
101
  throw error;
@@ -164,7 +164,7 @@ export const saveArticlesCommand = cli({
164
164
  description: 'Download WeChat official-account articles as Markdown files',
165
165
  strategy: Strategy.COOKIE, browser: browserRequired,
166
166
  args: [
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' },
167
+ { name: 'fakeid', positional: true, required: true, help: 'Official-account fakeid returned by weixin get-public-account-info' }, { name: 'name', help: 'Official-account name; exact case-insensitive match required for browser Sogou fallback' },
168
168
  { name: 'output', default: './weixin-articles', help: 'Directory for saved Markdown files' }, { name: 'limit', type: 'int', help: 'Maximum number of articles to save' },
169
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' },
170
170
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.53",
3
+ "version": "2.1.55",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },