@sovovs/bycli 2.1.51 → 2.1.52

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.
@@ -53,26 +53,50 @@ export function buildSelectTabScript(label) {
53
53
  const compact = value => String(value || '').trim().replace(/\\s+/g, ' ');
54
54
  const visible = node => {
55
55
  if (!node || node.hidden || node.closest('[hidden], [aria-hidden="true"]')) return false;
56
- const style = window.getComputedStyle(node);
57
- return style.display !== 'none' && style.visibility !== 'hidden';
56
+ for (let current = node; current && current.nodeType === 1; current = current.parentElement) {
57
+ const style = window.getComputedStyle(current);
58
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
59
+ }
60
+ return true;
58
61
  };
59
- const candidates = Array.from(document.querySelectorAll([
62
+ const selectors = [
63
+ 'a[href]',
64
+ 'button',
60
65
  '[role="tab"]',
61
66
  '.weui-desktop-tab__nav',
62
67
  '.tab_nav',
63
- 'a',
64
- 'button',
65
68
  'li',
66
- ].join(',')));
67
- const node = candidates.find(candidate => visible(candidate)
68
- && compact(candidate.textContent) === ${JSON.stringify(label)});
69
- if (!node) return { selected: false, disabled: false };
69
+ ];
70
+ let node = null;
71
+ for (const selector of selectors) {
72
+ node = Array.from(document.querySelectorAll(selector)).find(candidate => visible(candidate)
73
+ && compact(candidate.textContent) === ${JSON.stringify(label)});
74
+ if (node) break;
75
+ }
76
+ if (!node) return { selected: false, disabled: false, href: null };
70
77
  const disabled = node.disabled === true
71
78
  || node.getAttribute('aria-disabled') === 'true'
72
79
  || /(^|\\s)(disabled|is-disabled)(\\s|$)/.test(node.className || '');
73
- if (disabled) return { selected: false, disabled: true };
80
+ if (disabled) return { selected: false, disabled: true, href: null };
81
+ const rawHref = node.tagName === 'A' ? node.getAttribute('href') : null;
82
+ if (rawHref) {
83
+ const href = node.href;
84
+ let destination = null;
85
+ try { destination = new URL(href); } catch { /* handled as a click-only placeholder */ }
86
+ const trustedNavigation = destination
87
+ && destination.origin === window.location.origin
88
+ && destination.pathname === '/cgi-bin/settingpage'
89
+ && Boolean(destination.searchParams.get('token'));
90
+ if (trustedNavigation) return { selected: true, disabled: false, href };
91
+ const clickOnly = /^javascript:/i.test(rawHref)
92
+ || rawHref.charAt(0) === '#'
93
+ || (destination
94
+ && destination.origin === window.location.origin
95
+ && destination.pathname === '/cgi-bin/settingpage');
96
+ if (!clickOnly) return { selected: true, disabled: false, href };
97
+ }
74
98
  node.click();
75
- return { selected: true, disabled: false };
99
+ return { selected: true, disabled: false, href: null };
76
100
  })()`;
77
101
  }
78
102
 
@@ -84,8 +108,11 @@ export const SETTINGS_SESSION_SCRIPT = `(() => {
84
108
  ];
85
109
  const visible = node => {
86
110
  if (!node || node.hidden || node.closest('[hidden], [aria-hidden="true"]')) return false;
87
- const style = window.getComputedStyle(node);
88
- return style.display !== 'none' && style.visibility !== 'hidden';
111
+ for (let current = node; current && current.nodeType === 1; current = current.parentElement) {
112
+ const style = window.getComputedStyle(current);
113
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
114
+ }
115
+ return true;
89
116
  };
90
117
  return {
91
118
  href: window.location.href,
@@ -121,8 +148,11 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
121
148
  };
122
149
  const visible = node => {
123
150
  if (!node || node.hidden || node.closest('[hidden], [aria-hidden="true"]')) return false;
124
- const style = window.getComputedStyle(node);
125
- return style.display !== 'none' && style.visibility !== 'hidden';
151
+ for (let current = node; current && current.nodeType === 1; current = current.parentElement) {
152
+ const style = window.getComputedStyle(current);
153
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
154
+ }
155
+ return true;
126
156
  };
127
157
  const firstVisible = (root, selectors) => {
128
158
  for (const selector of selectors) {
@@ -186,8 +216,22 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
186
216
  const actions = [];
187
217
  const seenFields = new Set();
188
218
  const seenActions = new Set();
219
+ const fieldRows = new Set();
220
+ const genericParts = node => Array.from(node.children).flatMap(child => {
221
+ if (!visible(child)
222
+ || /^(H1|H2|H3|H4|H5|H6|A|BUTTON)$/.test(child.tagName)
223
+ || child.getAttribute('role') === 'button') return [];
224
+ const copy = child.cloneNode(true);
225
+ copy.querySelectorAll('a, button, [role="button"], svg, .setting_opr, .weui-desktop-setting__status, .setting_status, .status')
226
+ .forEach(item => item.remove());
227
+ const value = compact(copy.textContent);
228
+ return value ? [value] : [];
229
+ });
189
230
 
190
231
  for (const row of rows) {
232
+ const headerCells = row.tagName === 'TR' ? row.querySelectorAll(':scope > th') : [];
233
+ const dataCells = row.tagName === 'TR' ? row.querySelectorAll(':scope > td') : [];
234
+ if (headerCells.length > 0 && dataCells.length === 0) continue;
191
235
  const labelNode = firstVisible(row, [
192
236
  '.frm_label',
193
237
  '.weui-desktop-setting__label',
@@ -220,16 +264,48 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
220
264
  '.status',
221
265
  ]);
222
266
  let fieldValue = null;
267
+ let fieldStatus = compact(statusNode && statusNode.textContent);
223
268
  if (valueNode) {
224
269
  const copy = valueNode.cloneNode(true);
225
270
  copy.querySelectorAll('a, button, [role="button"], .setting_opr, .weui-desktop-setting__status, .setting_status, .status')
226
271
  .forEach(node => node.remove());
227
272
  fieldValue = compact(copy.textContent);
228
273
  }
274
+ if (fieldValue === null) {
275
+ const parts = genericParts(row);
276
+ const labelIndex = parts.indexOf(fieldLabel);
277
+ if (labelIndex !== -1) {
278
+ fieldValue = parts[labelIndex + 1] || null;
279
+ if (fieldStatus === null && parts.length > labelIndex + 2) {
280
+ fieldStatus = parts.slice(labelIndex + 2).join(' ');
281
+ }
282
+ }
283
+ }
229
284
  const item = {
230
285
  label: fieldLabel,
231
286
  value: fieldValue,
232
- status: compact(statusNode && statusNode.textContent),
287
+ status: fieldStatus,
288
+ };
289
+ const key = JSON.stringify(item);
290
+ if (!seenFields.has(key)) {
291
+ seenFields.add(key);
292
+ fields.push(item);
293
+ }
294
+ fieldRows.add(row);
295
+ }
296
+
297
+ let genericRows = Array.from(sectionNode.querySelectorAll('div, li'))
298
+ .filter(node => visible(node)
299
+ && !fieldRows.has(node)
300
+ && !rows.some(row => row !== node && row.contains(node))
301
+ && genericParts(node).length >= 2);
302
+ genericRows = genericRows.filter(node => !genericRows.some(other => other !== node && node.contains(other)));
303
+ for (const row of genericRows) {
304
+ const parts = genericParts(row);
305
+ const item = {
306
+ label: parts[0],
307
+ value: parts[1] || null,
308
+ status: parts.length > 2 ? parts.slice(2).join(' ') : null,
233
309
  };
234
310
  const key = JSON.stringify(item);
235
311
  if (!seenFields.has(key)) {
@@ -248,7 +324,9 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
248
324
  actions.push(item);
249
325
  }
250
326
  }
251
- return fields.length > 0 || actions.length > 0 ? [{ label, fields, actions }] : [];
327
+ if (fields.length > 0 || actions.length > 0) return [{ label, fields, actions }];
328
+ const empty = Boolean(sectionNode.querySelector('table thead th, table tr > th'));
329
+ return empty ? [{ label, fields, actions, empty: true }] : [];
252
330
  });
253
331
 
254
332
  if (sections.length === 0 && /(无权限|暂无权限|暂不支持|不可用)/.test(document.body.textContent || '')) {
@@ -305,7 +383,9 @@ export function normalizeUserInfoTab(tabId, payload) {
305
383
  const actions = rawActions.map((action, actionIndex) => (
306
384
  normalizeAction(action, definition.label, sectionIndex, actionIndex)
307
385
  ));
308
- return fields.length > 0 || actions.length > 0 ? [{ label, fields, actions }] : [];
386
+ return fields.length > 0 || actions.length > 0 || section.empty === true
387
+ ? [{ label, fields, actions }]
388
+ : [];
309
389
  });
310
390
 
311
391
  execution(sections.length > 0, `WeChat ${definition.label} exposed no recognizable settings`);
@@ -332,6 +412,20 @@ export async function collectUserInfoTabs(page, { settle = 2 } = {}) {
332
412
  });
333
413
  continue;
334
414
  }
415
+ if (selection.href !== null && selection.href !== undefined) {
416
+ let navigationUrl;
417
+ try {
418
+ navigationUrl = new URL(selection.href);
419
+ } catch {
420
+ throw new CommandExecutionError('WeChat account settings exposed an invalid tab destination');
421
+ }
422
+ execution(
423
+ navigationUrl.origin === ORIGIN
424
+ && Boolean(navigationUrl.searchParams.get('token')?.trim()),
425
+ 'WeChat account settings exposed an untrusted tab destination',
426
+ );
427
+ await page.goto(navigationUrl.toString());
428
+ }
335
429
  await page.wait(settle);
336
430
  const payload = await page.evaluate(USER_INFO_EXTRACT_SCRIPT);
337
431
  result.push({ id: definition.id, data: normalizeUserInfoTab(definition.id, payload) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.51",
3
+ "version": "2.1.52",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },