@sovovs/bycli 2.1.51 → 2.1.53

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) {
@@ -131,20 +161,73 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
131
161
  }
132
162
  return null;
133
163
  };
134
- const actionNodes = root => Array.from(root.querySelectorAll('a, button, [role="button"]'))
135
- .filter(node => {
136
- if (!visible(node)) return false;
137
- const label = compact(node.textContent || node.getAttribute('aria-label') || node.title);
138
- const hint = [node.className, node.title, node.getAttribute('aria-label')].join(' ');
139
- return label && !/(help|question|icon-question|帮助|说明)/i.test(hint);
140
- });
141
- const action = node => ({
142
- label: compact(node.textContent || node.getAttribute('aria-label') || node.title),
143
- enabled: !(node.disabled === true
144
- || node.getAttribute('aria-disabled') === 'true'
145
- || /(^|\\s)(disabled|is-disabled)(\\s|$)/.test(node.className || '')),
146
- href: node.tagName === 'A' && node.getAttribute('href') ? node.href : null,
147
- });
164
+ const ignoredValueSelector = [
165
+ 'a',
166
+ 'button',
167
+ '[role="button"]',
168
+ '[role="tooltip"]',
169
+ 'svg',
170
+ '.setting_opr',
171
+ '.weui-desktop-setting__status',
172
+ '.setting_status',
173
+ '.status',
174
+ '.frm_tips',
175
+ '.weui-desktop-form__tips',
176
+ '.weui-desktop-setting__desc',
177
+ '.setting_desc',
178
+ '.tips',
179
+ '.desc',
180
+ '.description',
181
+ '.help',
182
+ '.icon-question',
183
+ '[class*="question"]',
184
+ '[class*="help"]',
185
+ '[class*="popover"]',
186
+ '[class*="-ask"]',
187
+ ].join(',');
188
+ const cleanValueText = node => {
189
+ if (!node) return null;
190
+ const copy = node.cloneNode(true);
191
+ copy.querySelectorAll(ignoredValueSelector).forEach(item => item.remove());
192
+ return compact(copy.textContent);
193
+ };
194
+ const primaryValueText = node => {
195
+ if (node && node.matches('.weui-desktop-setting__item__info')) {
196
+ const directText = compact(Array.from(node.childNodes)
197
+ .filter(child => child.nodeType === Node.TEXT_NODE)
198
+ .map(child => child.textContent)
199
+ .join(' '));
200
+ if (directText) return cleanValueText(node);
201
+ const primaryChild = Array.from(node.children)
202
+ .find(child => visible(child) && !child.matches(ignoredValueSelector));
203
+ if (primaryChild) return cleanValueText(primaryChild);
204
+ }
205
+ return cleanValueText(node);
206
+ };
207
+ const switchValue = row => {
208
+ const node = firstVisible(row, [
209
+ 'input[type="checkbox"]',
210
+ '[role="switch"]',
211
+ '.weui-desktop-switch',
212
+ '.weui-switch',
213
+ '[class*="switch"]',
214
+ ]);
215
+ if (!node) return null;
216
+ const input = node.matches('input[type="checkbox"]')
217
+ ? node
218
+ : node.querySelector('input[type="checkbox"]');
219
+ if (input) return Boolean(input.checked);
220
+ const ariaNode = node.hasAttribute('aria-checked')
221
+ ? node
222
+ : node.querySelector('[aria-checked]');
223
+ const ariaChecked = ariaNode && ariaNode.getAttribute('aria-checked');
224
+ if (ariaChecked === 'true') return true;
225
+ if (ariaChecked === 'false') return false;
226
+ const className = [node.className, node.parentElement && node.parentElement.className].join(' ');
227
+ if (/(^|[\\s_-])(checked|on|active)([\\s_-]|$)/i.test(className)) return true;
228
+ if (/(^|[\\s_-])(unchecked|off)([\\s_-]|$)/i.test(className)) return false;
229
+ return null;
230
+ };
148
231
  const rowSelectors = [
149
232
  '.setting_item',
150
233
  '.weui-desktop-setting__item',
@@ -183,72 +266,82 @@ export const USER_INFO_EXTRACT_SCRIPT = `(() => {
183
266
  let rows = Array.from(sectionNode.querySelectorAll(rowSelector)).filter(visible);
184
267
  rows = rows.filter(node => !rows.some(other => other !== node && other.contains(node)));
185
268
  const fields = [];
186
- const actions = [];
187
269
  const seenFields = new Set();
188
- const seenActions = new Set();
270
+ const fieldRows = new Set();
271
+ const genericParts = node => Array.from(node.children).flatMap(child => {
272
+ if (!visible(child)
273
+ || /^(H1|H2|H3|H4|H5|H6|A|BUTTON)$/.test(child.tagName)
274
+ || child.getAttribute('role') === 'button') return [];
275
+ const value = cleanValueText(child);
276
+ return value ? [value] : [];
277
+ });
189
278
 
190
279
  for (const row of rows) {
280
+ const headerCells = row.tagName === 'TR' ? row.querySelectorAll(':scope > th') : [];
281
+ const dataCells = row.tagName === 'TR' ? row.querySelectorAll(':scope > td') : [];
282
+ if (headerCells.length > 0 && dataCells.length === 0) continue;
191
283
  const labelNode = firstVisible(row, [
192
284
  '.frm_label',
193
285
  '.weui-desktop-setting__label',
286
+ '.weui-desktop-setting__item__label',
194
287
  '.weui-desktop-form__label',
195
288
  'dt',
196
289
  'th',
197
290
  ]);
198
- const fieldLabel = compact(labelNode && labelNode.textContent);
199
- const rowActions = actionNodes(row);
200
- for (const node of rowActions) {
201
- const item = action(node);
202
- const key = JSON.stringify(item);
203
- if (!seenActions.has(key)) {
204
- seenActions.add(key);
205
- actions.push(item);
206
- }
207
- }
291
+ const fieldLabel = cleanValueText(labelNode);
208
292
  if (!fieldLabel) continue;
209
293
  const valueNode = firstVisible(row, [
210
294
  '.weui-desktop-setting__value',
295
+ '.weui-desktop-setting__item__info',
211
296
  '.setting_value',
212
297
  '.frm_value',
213
298
  'dd',
214
299
  'td',
215
300
  '.frm_controls',
216
301
  ]);
217
- const statusNode = firstVisible(row, [
218
- '.weui-desktop-setting__status',
219
- '.setting_status',
220
- '.status',
221
- ]);
222
- let fieldValue = null;
223
- if (valueNode) {
224
- const copy = valueNode.cloneNode(true);
225
- copy.querySelectorAll('a, button, [role="button"], .setting_opr, .weui-desktop-setting__status, .setting_status, .status')
226
- .forEach(node => node.remove());
227
- fieldValue = compact(copy.textContent);
302
+ let fieldValue = switchValue(row);
303
+ if (fieldValue === null) fieldValue = primaryValueText(valueNode);
304
+ if (fieldValue === null) {
305
+ const parts = genericParts(row);
306
+ const labelIndex = parts.indexOf(fieldLabel);
307
+ if (labelIndex !== -1) {
308
+ fieldValue = parts[labelIndex + 1] || null;
309
+ }
228
310
  }
229
311
  const item = {
230
312
  label: fieldLabel,
231
313
  value: fieldValue,
232
- status: compact(statusNode && statusNode.textContent),
233
314
  };
234
315
  const key = JSON.stringify(item);
235
316
  if (!seenFields.has(key)) {
236
317
  seenFields.add(key);
237
318
  fields.push(item);
238
319
  }
320
+ fieldRows.add(row);
239
321
  }
240
322
 
241
- const rowSet = new Set(rows);
242
- for (const node of actionNodes(sectionNode)) {
243
- if (rows.some(row => rowSet.has(row) && row.contains(node))) continue;
244
- const item = action(node);
323
+ let genericRows = Array.from(sectionNode.querySelectorAll('div, li'))
324
+ .filter(node => visible(node)
325
+ && !fieldRows.has(node)
326
+ && !rows.some(row => row !== node && row.contains(node))
327
+ && genericParts(node).length >= 2);
328
+ genericRows = genericRows.filter(node => !genericRows.some(other => other !== node && node.contains(other)));
329
+ for (const row of genericRows) {
330
+ const parts = genericParts(row);
331
+ const item = {
332
+ label: parts[0],
333
+ value: parts[1] || null,
334
+ };
245
335
  const key = JSON.stringify(item);
246
- if (!seenActions.has(key)) {
247
- seenActions.add(key);
248
- actions.push(item);
336
+ if (!seenFields.has(key)) {
337
+ seenFields.add(key);
338
+ fields.push(item);
249
339
  }
250
340
  }
251
- return fields.length > 0 || actions.length > 0 ? [{ label, fields, actions }] : [];
341
+
342
+ if (fields.length > 0) return [{ label, fields }];
343
+ const empty = Boolean(sectionNode.querySelector('table thead th, table tr > th'));
344
+ return empty ? [{ label, fields, empty: true }] : [];
252
345
  });
253
346
 
254
347
  if (sections.length === 0 && /(无权限|暂无权限|暂不支持|不可用)/.test(document.body.textContent || '')) {
@@ -264,20 +357,7 @@ function normalizeField(field, tabLabel, sectionIndex, fieldIndex) {
264
357
  execution(label, prefix);
265
358
  return {
266
359
  label,
267
- value: text(field.value),
268
- status: text(field.status),
269
- };
270
- }
271
-
272
- function normalizeAction(action, tabLabel, sectionIndex, actionIndex) {
273
- const prefix = `WeChat ${tabLabel} returned an invalid action at section ${sectionIndex} index ${actionIndex}`;
274
- execution(object(action), prefix);
275
- const label = text(action.label);
276
- execution(label, prefix);
277
- return {
278
- label,
279
- enabled: action.enabled !== false,
280
- path: sanitizeActionPath(action.href ?? action.path),
360
+ value: typeof field.value === 'boolean' ? field.value : text(field.value),
281
361
  };
282
362
  }
283
363
 
@@ -297,15 +377,13 @@ export function normalizeUserInfoTab(tabId, payload) {
297
377
  const label = text(section.label);
298
378
  execution(label, prefix);
299
379
  const rawFields = section.fields ?? [];
300
- const rawActions = section.actions ?? [];
301
- execution(Array.isArray(rawFields) && Array.isArray(rawActions), prefix);
380
+ execution(Array.isArray(rawFields), prefix);
302
381
  const fields = rawFields.map((field, fieldIndex) => (
303
382
  normalizeField(field, definition.label, sectionIndex, fieldIndex)
304
383
  ));
305
- const actions = rawActions.map((action, actionIndex) => (
306
- normalizeAction(action, definition.label, sectionIndex, actionIndex)
307
- ));
308
- return fields.length > 0 || actions.length > 0 ? [{ label, fields, actions }] : [];
384
+ return fields.length > 0 || section.empty === true
385
+ ? [{ label, fields }]
386
+ : [];
309
387
  });
310
388
 
311
389
  execution(sections.length > 0, `WeChat ${definition.label} exposed no recognizable settings`);
@@ -332,6 +410,20 @@ export async function collectUserInfoTabs(page, { settle = 2 } = {}) {
332
410
  });
333
411
  continue;
334
412
  }
413
+ if (selection.href !== null && selection.href !== undefined) {
414
+ let navigationUrl;
415
+ try {
416
+ navigationUrl = new URL(selection.href);
417
+ } catch {
418
+ throw new CommandExecutionError('WeChat account settings exposed an invalid tab destination');
419
+ }
420
+ execution(
421
+ navigationUrl.origin === ORIGIN
422
+ && Boolean(navigationUrl.searchParams.get('token')?.trim()),
423
+ 'WeChat account settings exposed an untrusted tab destination',
424
+ );
425
+ await page.goto(navigationUrl.toString());
426
+ }
335
427
  await page.wait(settle);
336
428
  const payload = await page.evaluate(USER_INFO_EXTRACT_SCRIPT);
337
429
  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.53",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },