@sovovs/bycli 2.1.49 → 2.1.51
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
|
@@ -29550,6 +29550,51 @@
|
|
|
29550
29550
|
"sourceFile": "weixin/home-overview.js",
|
|
29551
29551
|
"navigateBefore": false
|
|
29552
29552
|
},
|
|
29553
|
+
{
|
|
29554
|
+
"site": "weixin",
|
|
29555
|
+
"name": "open-platform-authorizer-info",
|
|
29556
|
+
"description": "Get an authorized Official Account profile through a Weixin third-party platform",
|
|
29557
|
+
"access": "read",
|
|
29558
|
+
"domain": "api.weixin.qq.com",
|
|
29559
|
+
"strategy": "local",
|
|
29560
|
+
"browser": false,
|
|
29561
|
+
"args": [
|
|
29562
|
+
{
|
|
29563
|
+
"name": "authorizerAppid",
|
|
29564
|
+
"type": "str",
|
|
29565
|
+
"required": true,
|
|
29566
|
+
"positional": true,
|
|
29567
|
+
"help": "Authorized Official Account AppID"
|
|
29568
|
+
},
|
|
29569
|
+
{
|
|
29570
|
+
"name": "component-appid",
|
|
29571
|
+
"type": "str",
|
|
29572
|
+
"required": false,
|
|
29573
|
+
"help": "Third-party platform AppID; falls back to WECHAT_COMPONENT_APPID"
|
|
29574
|
+
},
|
|
29575
|
+
{
|
|
29576
|
+
"name": "component-appsecret",
|
|
29577
|
+
"type": "str",
|
|
29578
|
+
"required": false,
|
|
29579
|
+
"help": "Third-party platform AppSecret; prefer WECHAT_COMPONENT_APPSECRET"
|
|
29580
|
+
},
|
|
29581
|
+
{
|
|
29582
|
+
"name": "component-verify-ticket",
|
|
29583
|
+
"type": "str",
|
|
29584
|
+
"required": false,
|
|
29585
|
+
"help": "Latest pushed verify ticket; prefer WECHAT_COMPONENT_VERIFY_TICKET"
|
|
29586
|
+
}
|
|
29587
|
+
],
|
|
29588
|
+
"columns": [
|
|
29589
|
+
"appid",
|
|
29590
|
+
"nickname",
|
|
29591
|
+
"username",
|
|
29592
|
+
"principal_name"
|
|
29593
|
+
],
|
|
29594
|
+
"type": "js",
|
|
29595
|
+
"modulePath": "weixin/open-platform-authorizer-info.js",
|
|
29596
|
+
"sourceFile": "weixin/open-platform-authorizer-info.js"
|
|
29597
|
+
},
|
|
29553
29598
|
{
|
|
29554
29599
|
"site": "weixin",
|
|
29555
29600
|
"name": "published",
|
|
@@ -29915,6 +29960,35 @@
|
|
|
29915
29960
|
"sourceFile": "weixin/user-growth.js",
|
|
29916
29961
|
"navigateBefore": false
|
|
29917
29962
|
},
|
|
29963
|
+
{
|
|
29964
|
+
"site": "weixin",
|
|
29965
|
+
"name": "user-info",
|
|
29966
|
+
"aliases": [
|
|
29967
|
+
"userInfo"
|
|
29968
|
+
],
|
|
29969
|
+
"description": "按账号详情、功能设置和授权管理 TAB 读取公众号账号设置",
|
|
29970
|
+
"access": "read",
|
|
29971
|
+
"domain": "mp.weixin.qq.com",
|
|
29972
|
+
"strategy": "cookie",
|
|
29973
|
+
"browser": true,
|
|
29974
|
+
"args": [
|
|
29975
|
+
{
|
|
29976
|
+
"name": "settle",
|
|
29977
|
+
"type": "int",
|
|
29978
|
+
"default": 2,
|
|
29979
|
+
"required": false,
|
|
29980
|
+
"help": "切换账号设置 TAB 后等待渲染的秒数"
|
|
29981
|
+
}
|
|
29982
|
+
],
|
|
29983
|
+
"columns": [
|
|
29984
|
+
"tab",
|
|
29985
|
+
"data_json"
|
|
29986
|
+
],
|
|
29987
|
+
"type": "js",
|
|
29988
|
+
"modulePath": "weixin/user-info.js",
|
|
29989
|
+
"sourceFile": "weixin/user-info.js",
|
|
29990
|
+
"navigateBefore": false
|
|
29991
|
+
},
|
|
29918
29992
|
{
|
|
29919
29993
|
"site": "weread",
|
|
29920
29994
|
"name": "ai-outline",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CommandExecutionError } from '@sovovs/bycli/errors';
|
|
2
|
+
|
|
3
|
+
const API_BASE = 'https://api.weixin.qq.com/cgi-bin/component';
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
|
|
6
|
+
function text(value) {
|
|
7
|
+
const normalized = String(value ?? '').trim();
|
|
8
|
+
return normalized || null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function redact(message, secrets = []) {
|
|
12
|
+
let redacted = String(message ?? '');
|
|
13
|
+
for (const secret of secrets) {
|
|
14
|
+
const value = text(secret);
|
|
15
|
+
if (!value) continue;
|
|
16
|
+
redacted = redacted.replaceAll(value, '[REDACTED]');
|
|
17
|
+
redacted = redacted.replaceAll(encodeURIComponent(value), '[REDACTED]');
|
|
18
|
+
}
|
|
19
|
+
redacted = redacted.replace(/([?&](?:component_access_token|access_token)=)[^&\s]*/giu, '$1[REDACTED]');
|
|
20
|
+
return redacted;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function postJson(path, body, { query = {}, fetchImpl = globalThis.fetch, secrets = [] } = {}) {
|
|
24
|
+
if (typeof fetchImpl !== 'function') throw new CommandExecutionError('Weixin Open Platform requires fetch support');
|
|
25
|
+
const url = new URL(`${API_BASE}/${path}`);
|
|
26
|
+
for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
|
|
27
|
+
let response;
|
|
28
|
+
try {
|
|
29
|
+
response = await fetchImpl(url.toString(), {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
throw new CommandExecutionError(`Weixin Open Platform ${path} request failed`);
|
|
37
|
+
}
|
|
38
|
+
let payload;
|
|
39
|
+
try {
|
|
40
|
+
payload = await response.json();
|
|
41
|
+
} catch {
|
|
42
|
+
throw new CommandExecutionError(`Weixin Open Platform ${path} returned invalid JSON`);
|
|
43
|
+
}
|
|
44
|
+
if (!response.ok) throw new CommandExecutionError(`Weixin Open Platform ${path} returned HTTP ${response.status}`);
|
|
45
|
+
if (payload?.errcode != null && payload.errcode !== 0) {
|
|
46
|
+
const message = redact(payload.errmsg || 'unknown error', secrets);
|
|
47
|
+
throw new CommandExecutionError(`Weixin Open Platform ${path} failed (${payload.errcode}): ${message}`);
|
|
48
|
+
}
|
|
49
|
+
return payload;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getComponentAccessToken({
|
|
53
|
+
componentAppid,
|
|
54
|
+
componentAppsecret,
|
|
55
|
+
componentVerifyTicket,
|
|
56
|
+
fetchImpl = globalThis.fetch,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const payload = await postJson('api_component_token', {
|
|
59
|
+
component_appid: componentAppid,
|
|
60
|
+
component_appsecret: componentAppsecret,
|
|
61
|
+
component_verify_ticket: componentVerifyTicket,
|
|
62
|
+
}, {
|
|
63
|
+
fetchImpl,
|
|
64
|
+
secrets: [componentAppsecret, componentVerifyTicket],
|
|
65
|
+
});
|
|
66
|
+
const accessToken = text(payload?.component_access_token);
|
|
67
|
+
if (!accessToken) {
|
|
68
|
+
throw new CommandExecutionError('Weixin Open Platform api_component_token returned no component access token');
|
|
69
|
+
}
|
|
70
|
+
return accessToken;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeAuthorizerProfile(payload, requestedAuthorizerAppid) {
|
|
74
|
+
const requested = text(requestedAuthorizerAppid);
|
|
75
|
+
const authorizerInfo = payload?.authorizer_info;
|
|
76
|
+
if (!requested || !authorizerInfo || typeof authorizerInfo !== 'object' || Array.isArray(authorizerInfo)) {
|
|
77
|
+
throw new CommandExecutionError('Weixin Open Platform returned an incomplete account profile');
|
|
78
|
+
}
|
|
79
|
+
const responseAppid = text(payload?.authorization_info?.authorizer_appid)
|
|
80
|
+
?? text(payload?.authorization_info?.authorization_appid);
|
|
81
|
+
if (responseAppid && responseAppid !== requested) {
|
|
82
|
+
throw new CommandExecutionError('Weixin Open Platform authorizer AppID did not match the request');
|
|
83
|
+
}
|
|
84
|
+
const nickname = text(authorizerInfo.nick_name);
|
|
85
|
+
const username = text(authorizerInfo.user_name);
|
|
86
|
+
const principalName = text(authorizerInfo.principal_name);
|
|
87
|
+
if (!nickname || !username || !principalName) {
|
|
88
|
+
throw new CommandExecutionError('Weixin Open Platform returned an incomplete account profile');
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
appid: requested,
|
|
92
|
+
nickname,
|
|
93
|
+
username,
|
|
94
|
+
principal_name: principalName,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function fetchAuthorizerProfile({
|
|
99
|
+
componentAppid,
|
|
100
|
+
componentAccessToken,
|
|
101
|
+
authorizerAppid,
|
|
102
|
+
fetchImpl = globalThis.fetch,
|
|
103
|
+
} = {}) {
|
|
104
|
+
const payload = await postJson('api_get_authorizer_info', {
|
|
105
|
+
component_appid: componentAppid,
|
|
106
|
+
authorizer_appid: authorizerAppid,
|
|
107
|
+
}, {
|
|
108
|
+
query: { component_access_token: componentAccessToken },
|
|
109
|
+
fetchImpl,
|
|
110
|
+
secrets: [componentAccessToken],
|
|
111
|
+
});
|
|
112
|
+
return normalizeAuthorizerProfile(payload, authorizerAppid);
|
|
113
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
2
|
+
|
|
3
|
+
const DOMAIN = 'mp.weixin.qq.com';
|
|
4
|
+
const ORIGIN = `https://${DOMAIN}`;
|
|
5
|
+
|
|
6
|
+
export const TAB_DEFINITIONS = Object.freeze([
|
|
7
|
+
Object.freeze({ id: 'account_details', label: '账号详情' }),
|
|
8
|
+
Object.freeze({ id: 'feature_settings', label: '功能设置' }),
|
|
9
|
+
Object.freeze({ id: 'authorization_management', label: '授权管理' }),
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function object(value) {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function execution(condition, message) {
|
|
17
|
+
if (!condition) throw new CommandExecutionError(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function text(value) {
|
|
21
|
+
if (value === null || value === undefined) return null;
|
|
22
|
+
const normalized = String(value).trim().replace(/\s+/g, ' ');
|
|
23
|
+
return normalized || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildSettingsUrl(token) {
|
|
27
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
28
|
+
throw new ArgumentError('token is required');
|
|
29
|
+
}
|
|
30
|
+
const parameters = new URLSearchParams({
|
|
31
|
+
t: 'setting/index',
|
|
32
|
+
action: 'index',
|
|
33
|
+
token,
|
|
34
|
+
lang: 'zh_CN',
|
|
35
|
+
});
|
|
36
|
+
return `${ORIGIN}/cgi-bin/settingpage?${parameters}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sanitizeActionPath(value) {
|
|
40
|
+
if (typeof value !== 'string' || !value.trim() || /^javascript:/i.test(value.trim())) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(value, ORIGIN);
|
|
45
|
+
return url.origin === ORIGIN ? url.pathname : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildSelectTabScript(label) {
|
|
52
|
+
return `(() => {
|
|
53
|
+
const compact = value => String(value || '').trim().replace(/\\s+/g, ' ');
|
|
54
|
+
const visible = node => {
|
|
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';
|
|
58
|
+
};
|
|
59
|
+
const candidates = Array.from(document.querySelectorAll([
|
|
60
|
+
'[role="tab"]',
|
|
61
|
+
'.weui-desktop-tab__nav',
|
|
62
|
+
'.tab_nav',
|
|
63
|
+
'a',
|
|
64
|
+
'button',
|
|
65
|
+
'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 };
|
|
70
|
+
const disabled = node.disabled === true
|
|
71
|
+
|| node.getAttribute('aria-disabled') === 'true'
|
|
72
|
+
|| /(^|\\s)(disabled|is-disabled)(\\s|$)/.test(node.className || '');
|
|
73
|
+
if (disabled) return { selected: false, disabled: true };
|
|
74
|
+
node.click();
|
|
75
|
+
return { selected: true, disabled: false };
|
|
76
|
+
})()`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const SETTINGS_SESSION_SCRIPT = `(() => {
|
|
80
|
+
const selectors = [
|
|
81
|
+
'form[action*="login"]',
|
|
82
|
+
'img[src*="qrcode"]',
|
|
83
|
+
'canvas[class*="qrcode"]',
|
|
84
|
+
];
|
|
85
|
+
const visible = node => {
|
|
86
|
+
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';
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
href: window.location.href,
|
|
92
|
+
hasLoginUi: selectors.some(selector => Array.from(document.querySelectorAll(selector)).some(visible))
|
|
93
|
+
|| /扫码登录|使用微信扫码/.test(document.body && document.body.innerText || ''),
|
|
94
|
+
};
|
|
95
|
+
})()`;
|
|
96
|
+
|
|
97
|
+
export function assertSettingsSessionState(payload) {
|
|
98
|
+
execution(object(payload) && typeof payload.href === 'string',
|
|
99
|
+
'WeChat account settings returned an unreadable page state');
|
|
100
|
+
if (payload.hasLoginUi === true) {
|
|
101
|
+
throw new AuthRequiredError(DOMAIN, 'WeChat account settings login has expired');
|
|
102
|
+
}
|
|
103
|
+
let url;
|
|
104
|
+
try {
|
|
105
|
+
url = new URL(payload.href);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new CommandExecutionError('WeChat account settings returned an invalid page URL');
|
|
108
|
+
}
|
|
109
|
+
if (url.origin === ORIGIN
|
|
110
|
+
&& (url.pathname === '/' || /login/i.test(url.pathname) || !url.searchParams.get('token')?.trim())) {
|
|
111
|
+
throw new AuthRequiredError(DOMAIN, 'WeChat account settings login has expired');
|
|
112
|
+
}
|
|
113
|
+
execution(url.origin === ORIGIN && url.pathname === '/cgi-bin/settingpage',
|
|
114
|
+
'WeChat account settings redirected to an unexpected page');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const USER_INFO_EXTRACT_SCRIPT = `(() => {
|
|
118
|
+
const compact = value => {
|
|
119
|
+
const normalized = String(value || '').trim().replace(/\\s+/g, ' ');
|
|
120
|
+
return normalized || null;
|
|
121
|
+
};
|
|
122
|
+
const visible = node => {
|
|
123
|
+
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';
|
|
126
|
+
};
|
|
127
|
+
const firstVisible = (root, selectors) => {
|
|
128
|
+
for (const selector of selectors) {
|
|
129
|
+
const matched = Array.from(root.querySelectorAll(selector)).find(visible);
|
|
130
|
+
if (matched) return matched;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
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
|
+
});
|
|
148
|
+
const rowSelectors = [
|
|
149
|
+
'.setting_item',
|
|
150
|
+
'.weui-desktop-setting__item',
|
|
151
|
+
'.weui-desktop-form__control-group',
|
|
152
|
+
'tr',
|
|
153
|
+
'dl',
|
|
154
|
+
];
|
|
155
|
+
const sectionSelectors = [
|
|
156
|
+
'.setting_area',
|
|
157
|
+
'.weui-desktop-panel',
|
|
158
|
+
'.weui-desktop-card',
|
|
159
|
+
'section',
|
|
160
|
+
];
|
|
161
|
+
const rowSelector = rowSelectors.join(',');
|
|
162
|
+
let sectionNodes = Array.from(document.querySelectorAll(sectionSelectors.join(',')))
|
|
163
|
+
.filter(visible);
|
|
164
|
+
sectionNodes = sectionNodes.filter(node => !sectionNodes.some(other => other !== node && other.contains(node)));
|
|
165
|
+
if (sectionNodes.length === 0) {
|
|
166
|
+
const fallbackRows = Array.from(document.querySelectorAll(rowSelector)).filter(visible);
|
|
167
|
+
if (fallbackRows.length > 0) sectionNodes = [document.body];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const sections = sectionNodes.flatMap(sectionNode => {
|
|
171
|
+
const heading = firstVisible(sectionNode, [
|
|
172
|
+
'.setting_area_hd h3',
|
|
173
|
+
'.setting_area_hd',
|
|
174
|
+
'.weui-desktop-panel__title',
|
|
175
|
+
'.weui-desktop-card__title',
|
|
176
|
+
'h2',
|
|
177
|
+
'h3',
|
|
178
|
+
'h4',
|
|
179
|
+
]);
|
|
180
|
+
const label = compact(heading && heading.textContent)
|
|
181
|
+
|| compact(sectionNode.getAttribute('aria-label'))
|
|
182
|
+
|| '其他信息';
|
|
183
|
+
let rows = Array.from(sectionNode.querySelectorAll(rowSelector)).filter(visible);
|
|
184
|
+
rows = rows.filter(node => !rows.some(other => other !== node && other.contains(node)));
|
|
185
|
+
const fields = [];
|
|
186
|
+
const actions = [];
|
|
187
|
+
const seenFields = new Set();
|
|
188
|
+
const seenActions = new Set();
|
|
189
|
+
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
const labelNode = firstVisible(row, [
|
|
192
|
+
'.frm_label',
|
|
193
|
+
'.weui-desktop-setting__label',
|
|
194
|
+
'.weui-desktop-form__label',
|
|
195
|
+
'dt',
|
|
196
|
+
'th',
|
|
197
|
+
]);
|
|
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
|
+
}
|
|
208
|
+
if (!fieldLabel) continue;
|
|
209
|
+
const valueNode = firstVisible(row, [
|
|
210
|
+
'.weui-desktop-setting__value',
|
|
211
|
+
'.setting_value',
|
|
212
|
+
'.frm_value',
|
|
213
|
+
'dd',
|
|
214
|
+
'td',
|
|
215
|
+
'.frm_controls',
|
|
216
|
+
]);
|
|
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);
|
|
228
|
+
}
|
|
229
|
+
const item = {
|
|
230
|
+
label: fieldLabel,
|
|
231
|
+
value: fieldValue,
|
|
232
|
+
status: compact(statusNode && statusNode.textContent),
|
|
233
|
+
};
|
|
234
|
+
const key = JSON.stringify(item);
|
|
235
|
+
if (!seenFields.has(key)) {
|
|
236
|
+
seenFields.add(key);
|
|
237
|
+
fields.push(item);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
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);
|
|
245
|
+
const key = JSON.stringify(item);
|
|
246
|
+
if (!seenActions.has(key)) {
|
|
247
|
+
seenActions.add(key);
|
|
248
|
+
actions.push(item);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return fields.length > 0 || actions.length > 0 ? [{ label, fields, actions }] : [];
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (sections.length === 0 && /(无权限|暂无权限|暂不支持|不可用)/.test(document.body.textContent || '')) {
|
|
255
|
+
return { available: false, sections: [] };
|
|
256
|
+
}
|
|
257
|
+
return { available: true, sections };
|
|
258
|
+
})()`;
|
|
259
|
+
|
|
260
|
+
function normalizeField(field, tabLabel, sectionIndex, fieldIndex) {
|
|
261
|
+
const prefix = `WeChat ${tabLabel} returned an invalid field at section ${sectionIndex} index ${fieldIndex}`;
|
|
262
|
+
execution(object(field), prefix);
|
|
263
|
+
const label = text(field.label);
|
|
264
|
+
execution(label, prefix);
|
|
265
|
+
return {
|
|
266
|
+
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),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function normalizeUserInfoTab(tabId, payload) {
|
|
285
|
+
const definition = TAB_DEFINITIONS.find(tab => tab.id === tabId);
|
|
286
|
+
execution(definition, `WeChat account settings returned an unknown tab: ${String(tabId)}`);
|
|
287
|
+
execution(object(payload), `WeChat ${definition.label} returned an unreadable payload`);
|
|
288
|
+
|
|
289
|
+
if (payload.available === false) {
|
|
290
|
+
return { label: definition.label, available: false, sections: [] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
execution(Array.isArray(payload.sections), `WeChat ${definition.label} returned an invalid section list`);
|
|
294
|
+
const sections = payload.sections.flatMap((section, sectionIndex) => {
|
|
295
|
+
const prefix = `WeChat ${definition.label} returned an invalid section at index ${sectionIndex}`;
|
|
296
|
+
execution(object(section), prefix);
|
|
297
|
+
const label = text(section.label);
|
|
298
|
+
execution(label, prefix);
|
|
299
|
+
const rawFields = section.fields ?? [];
|
|
300
|
+
const rawActions = section.actions ?? [];
|
|
301
|
+
execution(Array.isArray(rawFields) && Array.isArray(rawActions), prefix);
|
|
302
|
+
const fields = rawFields.map((field, fieldIndex) => (
|
|
303
|
+
normalizeField(field, definition.label, sectionIndex, fieldIndex)
|
|
304
|
+
));
|
|
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 }] : [];
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
execution(sections.length > 0, `WeChat ${definition.label} exposed no recognizable settings`);
|
|
312
|
+
return { label: definition.label, available: true, sections };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function collectUserInfoTabs(page, { settle = 2 } = {}) {
|
|
316
|
+
const result = [];
|
|
317
|
+
let recognizedTabs = 0;
|
|
318
|
+
for (const definition of TAB_DEFINITIONS) {
|
|
319
|
+
const selection = await page.evaluate(buildSelectTabScript(definition.label));
|
|
320
|
+
if (!selection?.selected && !selection?.disabled) {
|
|
321
|
+
result.push({
|
|
322
|
+
id: definition.id,
|
|
323
|
+
data: normalizeUserInfoTab(definition.id, { available: false, sections: [] }),
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
recognizedTabs += 1;
|
|
328
|
+
if (selection.disabled) {
|
|
329
|
+
result.push({
|
|
330
|
+
id: definition.id,
|
|
331
|
+
data: normalizeUserInfoTab(definition.id, { available: false, sections: [] }),
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
await page.wait(settle);
|
|
336
|
+
const payload = await page.evaluate(USER_INFO_EXTRACT_SCRIPT);
|
|
337
|
+
result.push({ id: definition.id, data: normalizeUserInfoTab(definition.id, payload) });
|
|
338
|
+
}
|
|
339
|
+
if (recognizedTabs === 0) {
|
|
340
|
+
throw new CommandExecutionError(
|
|
341
|
+
'WeChat account settings did not expose any recognized tabs',
|
|
342
|
+
'Reload the Official Account settings page and run the command again.',
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ArgumentError } from '@sovovs/bycli/errors';
|
|
2
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
3
|
+
import { fetchAuthorizerProfile, getComponentAccessToken } from './_wechat/open-platform-api.js';
|
|
4
|
+
|
|
5
|
+
function text(value) {
|
|
6
|
+
const normalized = String(value ?? '').trim();
|
|
7
|
+
return normalized || null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function readOpenPlatformCredentials(args = {}, env = process.env) {
|
|
11
|
+
const componentAppid = text(args['component-appid']) ?? text(env.WECHAT_COMPONENT_APPID);
|
|
12
|
+
const componentAppsecret = text(args['component-appsecret']) ?? text(env.WECHAT_COMPONENT_APPSECRET);
|
|
13
|
+
const componentVerifyTicket = text(args['component-verify-ticket']) ?? text(env.WECHAT_COMPONENT_VERIFY_TICKET);
|
|
14
|
+
if (!componentAppid || !componentAppsecret || !componentVerifyTicket) {
|
|
15
|
+
throw new ArgumentError('Weixin Open Platform requires component AppID, AppSecret, and verify ticket');
|
|
16
|
+
}
|
|
17
|
+
return { componentAppid, componentAppsecret, componentVerifyTicket };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const openPlatformAuthorizerInfoCommand = cli({
|
|
21
|
+
site: 'weixin',
|
|
22
|
+
name: 'open-platform-authorizer-info',
|
|
23
|
+
access: 'read',
|
|
24
|
+
domain: 'api.weixin.qq.com',
|
|
25
|
+
description: 'Get an authorized Official Account profile through a Weixin third-party platform',
|
|
26
|
+
strategy: Strategy.LOCAL,
|
|
27
|
+
browser: false,
|
|
28
|
+
args: [
|
|
29
|
+
{
|
|
30
|
+
name: 'authorizerAppid',
|
|
31
|
+
positional: true,
|
|
32
|
+
required: true,
|
|
33
|
+
help: 'Authorized Official Account AppID',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'component-appid',
|
|
37
|
+
help: 'Third-party platform AppID; falls back to WECHAT_COMPONENT_APPID',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'component-appsecret',
|
|
41
|
+
help: 'Third-party platform AppSecret; prefer WECHAT_COMPONENT_APPSECRET',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'component-verify-ticket',
|
|
45
|
+
help: 'Latest pushed verify ticket; prefer WECHAT_COMPONENT_VERIFY_TICKET',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
columns: ['appid', 'nickname', 'username', 'principal_name'],
|
|
49
|
+
func: async args => {
|
|
50
|
+
const authorizerAppid = text(args.authorizerAppid);
|
|
51
|
+
if (!authorizerAppid) throw new ArgumentError('authorizerAppid is required');
|
|
52
|
+
const credentials = readOpenPlatformCredentials(args);
|
|
53
|
+
const componentAccessToken = await getComponentAccessToken(credentials);
|
|
54
|
+
const profile = await fetchAuthorizerProfile({
|
|
55
|
+
componentAppid: credentials.componentAppid,
|
|
56
|
+
componentAccessToken,
|
|
57
|
+
authorizerAppid,
|
|
58
|
+
});
|
|
59
|
+
return [profile];
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
2
|
+
import { resolveBrowserCredentials } from './_wechat/auth-session.js';
|
|
3
|
+
import {
|
|
4
|
+
SETTINGS_SESSION_SCRIPT,
|
|
5
|
+
assertSettingsSessionState,
|
|
6
|
+
buildSettingsUrl,
|
|
7
|
+
collectUserInfoTabs,
|
|
8
|
+
} from './_wechat/user-info.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SETTLE_SECONDS = 2;
|
|
11
|
+
|
|
12
|
+
function settleSeconds(value) {
|
|
13
|
+
const parsed = Number(value ?? DEFAULT_SETTLE_SECONDS);
|
|
14
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_SETTLE_SECONDS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const userInfoCommand = cli({
|
|
18
|
+
site: 'weixin',
|
|
19
|
+
name: 'user-info',
|
|
20
|
+
aliases: ['userInfo'],
|
|
21
|
+
access: 'read',
|
|
22
|
+
domain: 'mp.weixin.qq.com',
|
|
23
|
+
description: '按账号详情、功能设置和授权管理 TAB 读取公众号账号设置',
|
|
24
|
+
strategy: Strategy.COOKIE,
|
|
25
|
+
browser: true,
|
|
26
|
+
navigateBefore: false,
|
|
27
|
+
args: [
|
|
28
|
+
{
|
|
29
|
+
name: 'settle',
|
|
30
|
+
type: 'int',
|
|
31
|
+
default: DEFAULT_SETTLE_SECONDS,
|
|
32
|
+
help: '切换账号设置 TAB 后等待渲染的秒数',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
columns: ['tab', 'data_json'],
|
|
36
|
+
func: async (page, args) => {
|
|
37
|
+
const { token } = await resolveBrowserCredentials(page);
|
|
38
|
+
const settle = settleSeconds(args.settle);
|
|
39
|
+
await page.goto(buildSettingsUrl(token));
|
|
40
|
+
await page.wait(settle);
|
|
41
|
+
assertSettingsSessionState(await page.evaluate(SETTINGS_SESSION_SCRIPT));
|
|
42
|
+
const tabs = await collectUserInfoTabs(page, { settle });
|
|
43
|
+
return tabs.map(tab => ({
|
|
44
|
+
tab: tab.id,
|
|
45
|
+
data_json: JSON.stringify(tab.data),
|
|
46
|
+
}));
|
|
47
|
+
},
|
|
48
|
+
});
|