@sovovs/bycli 2.1.25 → 2.1.27
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/clis/weixin/_wechat/article-fallback-policy.js +14 -3
- package/clis/weixin/_wechat/article-index.js +7 -3
- package/clis/weixin/_wechat/sogou-fallback.js +47 -5
- package/clis/weixin/_wechat/sogou-search.js +78 -43
- package/clis/weixin/articles.js +1 -1
- package/clis/weixin/create-draft.js +9 -3
- package/clis/weixin/save-articles.js +1 -1
- package/dist/src/errors.d.ts +4 -1
- package/dist/src/errors.js +5 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AuthRequiredError, CommandExecutionError, EmptyResultError,
|
|
2
|
+
AuthRequiredError, CliError, CommandExecutionError, EmptyResultError, RateLimitedError,
|
|
3
3
|
} from '@sovovs/bycli/errors';
|
|
4
4
|
import { buildSecretSet, redactText } from './redact.js';
|
|
5
5
|
|
|
@@ -16,12 +16,17 @@ function safePhase(error, secrets) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function isEligibleArticleFallbackError(error) {
|
|
19
|
-
return error instanceof CommandExecutionError
|
|
19
|
+
return error instanceof CommandExecutionError
|
|
20
|
+
|| error instanceof EmptyResultError
|
|
21
|
+
|| (error instanceof CliError && error.code === 'RATE_LIMITED');
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function withMissingFallbackName(operation, error) {
|
|
23
25
|
const hint = `${error.hint ? `${error.hint} ` : ''}Sogou fallback requires the exact official-account name in --name.`;
|
|
24
26
|
if (error instanceof EmptyResultError) return new EmptyResultError(operation, hint);
|
|
27
|
+
if (error instanceof CliError && error.code === 'RATE_LIMITED') {
|
|
28
|
+
return new RateLimitedError(error.message, hint);
|
|
29
|
+
}
|
|
25
30
|
return new CommandExecutionError(error.message, hint);
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -31,11 +36,17 @@ export function combineArticleFallbackErrors({
|
|
|
31
36
|
fallbackError,
|
|
32
37
|
credentials,
|
|
33
38
|
}) {
|
|
34
|
-
if (fallbackError instanceof AuthRequiredError) return fallbackError;
|
|
35
39
|
const secrets = buildSecretSet(credentials);
|
|
36
40
|
const primary = safePhase(primaryError, secrets);
|
|
37
41
|
const fallback = safePhase(fallbackError, secrets);
|
|
38
42
|
const hint = `Primary (${primary.code}): ${primary.summary}; fallback (${fallback.code}): ${fallback.summary}`;
|
|
43
|
+
if (primaryError instanceof CliError && primaryError.code === 'RATE_LIMITED') {
|
|
44
|
+
return new RateLimitedError(
|
|
45
|
+
'Weixin article index was rate limited and Sogou fallback failed',
|
|
46
|
+
hint,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (fallbackError instanceof AuthRequiredError) return fallbackError;
|
|
39
50
|
if (primaryError instanceof EmptyResultError && fallbackError instanceof EmptyResultError) {
|
|
40
51
|
return new EmptyResultError(operation, hint);
|
|
41
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
1
|
+
import { AuthRequiredError, CommandExecutionError, RateLimitedError } from '@sovovs/bycli/errors';
|
|
2
2
|
import { buildSecretSet, redactText } from './redact.js';
|
|
3
3
|
|
|
4
4
|
const DOMAIN = 'mp.weixin.qq.com';
|
|
@@ -34,8 +34,8 @@ export function mapArticleIndexPayload(payload) {
|
|
|
34
34
|
throw new AuthRequiredError(DOMAIN, 'WeChat article-index credentials have expired');
|
|
35
35
|
}
|
|
36
36
|
if (ret === 200013 && normalizedMessage === 'freq control') {
|
|
37
|
-
throw
|
|
38
|
-
'was rate limited (ret=200013)',
|
|
37
|
+
throw new RateLimitedError(
|
|
38
|
+
'WeChat appmsgpublish was rate limited (ret=200013)',
|
|
39
39
|
'Wait before retrying the WeChat article-index request; repeated retries may extend frequency control.',
|
|
40
40
|
);
|
|
41
41
|
}
|
|
@@ -122,6 +122,10 @@ function transportError(error, credentials) {
|
|
|
122
122
|
const redactedHint = hint ? redactText(hint, secrets) : undefined;
|
|
123
123
|
if (error instanceof AuthRequiredError && error.domain === DOMAIN
|
|
124
124
|
&& redactedMessage === message && redactedHint === hint) return error;
|
|
125
|
+
if (error instanceof RateLimitedError) {
|
|
126
|
+
if (redactedMessage === message && redactedHint === hint) return error;
|
|
127
|
+
return new RateLimitedError(redactedMessage, redactedHint);
|
|
128
|
+
}
|
|
125
129
|
if (isWechatVerificationResponse(redactedMessage, redactedHint)) {
|
|
126
130
|
return new AuthRequiredError(
|
|
127
131
|
DOMAIN,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
import { resolveWechatArticleUrl } from './article-link.js';
|
|
5
5
|
import { redactText } from './redact.js';
|
|
6
6
|
import {
|
|
7
|
+
buildSogouSearchUrl,
|
|
7
8
|
DEFAULT_SOGOU_MAX_PAGES,
|
|
8
9
|
normalizePositiveInteger,
|
|
9
10
|
searchSogouArticlePage,
|
|
@@ -92,6 +93,32 @@ function safeResolutionError(error) {
|
|
|
92
93
|
return redactText(message, []) || 'Sogou article link resolution failed';
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
async function replaceWithFreshSogouPage(page, accountName) {
|
|
97
|
+
if (typeof page?.closeWindow !== 'function'
|
|
98
|
+
|| typeof page?.newTab !== 'function'
|
|
99
|
+
|| typeof page?.setActivePage !== 'function') {
|
|
100
|
+
throw new CommandExecutionError(
|
|
101
|
+
'weixin Sogou fallback cannot create an isolated browser page',
|
|
102
|
+
'Update byCLI and Browser Bridge, then retry the command.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const searchUrl = buildSogouSearchUrl(accountName, 1);
|
|
106
|
+
let createdPage;
|
|
107
|
+
try {
|
|
108
|
+
await page.closeWindow();
|
|
109
|
+
createdPage = await page.newTab(searchUrl);
|
|
110
|
+
if (!createdPage) throw new Error('Browser Bridge returned no page identity');
|
|
111
|
+
page.setActivePage(createdPage);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
114
|
+
throw new CommandExecutionError(
|
|
115
|
+
'weixin Sogou fallback failed to create an isolated browser page',
|
|
116
|
+
detail,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return searchUrl;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
export async function collectSogouAccountArticles({
|
|
96
123
|
page,
|
|
97
124
|
accountName,
|
|
@@ -101,6 +128,7 @@ export async function collectSogouAccountArticles({
|
|
|
101
128
|
resolveUrl = resolveWechatArticleUrl,
|
|
102
129
|
resolutionPolicy = 'atomic',
|
|
103
130
|
scanStartedAt = Date.now(),
|
|
131
|
+
freshPage = false,
|
|
104
132
|
}) {
|
|
105
133
|
const normalizedName = String(accountName ?? '').trim();
|
|
106
134
|
if (!normalizedName) {
|
|
@@ -125,12 +153,21 @@ export async function collectSogouAccountArticles({
|
|
|
125
153
|
let pagesScanned = 0;
|
|
126
154
|
let coverage = 'max-pages-reached';
|
|
127
155
|
let firstSeen = 0;
|
|
156
|
+
const preloadedFirstPageUrl = freshPage
|
|
157
|
+
? await replaceWithFreshSogouPage(page, normalizedName)
|
|
158
|
+
: undefined;
|
|
128
159
|
|
|
129
160
|
for (let pageNo = 1; pageNo <= pageLimit; pageNo += 1) {
|
|
130
|
-
const result = await searchPage(page, {
|
|
161
|
+
const result = await searchPage(page, {
|
|
162
|
+
query: normalizedName,
|
|
163
|
+
pageNo,
|
|
164
|
+
preloadedUrl: pageNo === 1 ? preloadedFirstPageUrl : undefined,
|
|
165
|
+
});
|
|
131
166
|
pagesScanned += 1;
|
|
132
167
|
if (result.state === 'empty') {
|
|
133
|
-
coverage = '
|
|
168
|
+
coverage = result.reason === 'result-cap'
|
|
169
|
+
? 'result-cap-reached'
|
|
170
|
+
: 'search-exhausted';
|
|
134
171
|
break;
|
|
135
172
|
}
|
|
136
173
|
if (seenFingerprints.has(result.fingerprint)) {
|
|
@@ -160,9 +197,14 @@ export async function collectSogouAccountArticles({
|
|
|
160
197
|
}
|
|
161
198
|
|
|
162
199
|
if (candidates.length === 0) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
200
|
+
let coverageHint;
|
|
201
|
+
if (coverage === 'max-pages-reached') {
|
|
202
|
+
coverageHint = `Scanned ${pagesScanned} pages and reached the page cap; later pages may still contain a match.`;
|
|
203
|
+
} else if (coverage === 'result-cap-reached') {
|
|
204
|
+
coverageHint = 'Sogou stopped anonymous browsing at its 100-result visibility cap; hidden results may still contain a match.';
|
|
205
|
+
} else {
|
|
206
|
+
coverageHint = `Sogou search exhausted after ${pagesScanned} pages.`;
|
|
207
|
+
}
|
|
166
208
|
throw new EmptyResultError(
|
|
167
209
|
'weixin Sogou account fallback',
|
|
168
210
|
`No Sogou articles matched the exact official-account name "${normalizedName}"; similarly named accounts were excluded. ${coverageHint}`,
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
|
|
5
5
|
const SOGOU_WEIXIN_DOMAIN = 'weixin.sogou.com';
|
|
6
6
|
export const DEFAULT_SOGOU_MAX_PAGES = 50;
|
|
7
|
+
const MAX_SOGOU_SHELL_ATTEMPTS = 2;
|
|
8
|
+
const SOGOU_SHELL_RETRY_DELAY_SECONDS = 2;
|
|
7
9
|
|
|
8
10
|
export function normalizePositiveInteger(value, name, defaultValue, maxValue) {
|
|
9
11
|
if (value === undefined || value === null) return defaultValue;
|
|
@@ -52,9 +54,11 @@ export function buildExtractSogouSearchResultsEvaluate() {
|
|
|
52
54
|
}
|
|
53
55
|
};
|
|
54
56
|
|
|
55
|
-
const bodyText = clean(document.body && document.body.innerText);
|
|
57
|
+
const bodyText = clean(document.body && (document.body.innerText || document.body.textContent));
|
|
56
58
|
const blocked = /验证码|安全验证|异常访问|访问过于频繁|请输入验证码/.test(bodyText);
|
|
57
|
-
const
|
|
59
|
+
const resultCap = /当前只显示\s*100\s*条结果/.test(bodyText);
|
|
60
|
+
const empty = resultCap
|
|
61
|
+
|| /没有找到相关的微信文章|未找到相关|暂无相关|没有找到/.test(bodyText)
|
|
58
62
|
|| Boolean(document.querySelector('.no-result, .no_result, .s-noresult'));
|
|
59
63
|
const cards = Array.from(document.querySelectorAll('.news-list li'));
|
|
60
64
|
const extracted = cards.map((item) => {
|
|
@@ -78,6 +82,7 @@ export function buildExtractSogouSearchResultsEvaluate() {
|
|
|
78
82
|
return {
|
|
79
83
|
blocked,
|
|
80
84
|
empty,
|
|
85
|
+
resultCap,
|
|
81
86
|
invalidCount: extracted.length - rows.length,
|
|
82
87
|
rows,
|
|
83
88
|
};
|
|
@@ -88,56 +93,86 @@ function fingerprintRows(rows) {
|
|
|
88
93
|
return rows.map(row => `${row.title}\u0000${row.url}`).join('\u0001');
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
const normalizedPage = normalizePositiveInteger(pageNo, 'page', 1);
|
|
100
|
-
const searchUrl = buildSogouSearchUrl(normalizedQuery, normalizedPage);
|
|
101
|
-
let payload;
|
|
96
|
+
function buildSogouRetryUrl(searchUrl, retryNo) {
|
|
97
|
+
const retryUrl = new URL(searchUrl);
|
|
98
|
+
retryUrl.searchParams.set('_bycli_retry', String(retryNo));
|
|
99
|
+
return retryUrl.toString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadSogouPayload(page, searchUrl, delayBeforeSeconds = 0, navigate = true) {
|
|
102
103
|
try {
|
|
103
|
-
await page.
|
|
104
|
+
if (delayBeforeSeconds > 0) await page.wait(delayBeforeSeconds);
|
|
105
|
+
if (navigate) await page.goto(searchUrl);
|
|
104
106
|
await page.wait(2);
|
|
105
|
-
|
|
107
|
+
return await page.evaluate(buildExtractSogouSearchResultsEvaluate());
|
|
106
108
|
} catch (error) {
|
|
107
109
|
const detail = error instanceof Error ? error.message : String(error);
|
|
108
|
-
throw new CommandExecutionError('weixin sougousearch failed while loading Sogou results', detail);
|
|
109
|
-
}
|
|
110
|
-
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.rows)) {
|
|
111
110
|
throw new CommandExecutionError(
|
|
112
|
-
'weixin sougousearch
|
|
113
|
-
|
|
111
|
+
'weixin sougousearch failed while loading Sogou results',
|
|
112
|
+
detail,
|
|
114
113
|
);
|
|
115
114
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'Sogou Weixin returned article cards without required title or URL',
|
|
125
|
-
'The result page structure may have changed; refusing to return a partial result set.',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function searchSogouArticlePage(page, { query, pageNo, preloadedUrl }) {
|
|
118
|
+
const normalizedQuery = String(query ?? '').trim();
|
|
119
|
+
if (!normalizedQuery) {
|
|
120
|
+
throw new ArgumentError(
|
|
121
|
+
'A search query is required.',
|
|
122
|
+
'Pass a non-empty keyword to search Weixin articles via Sogou.',
|
|
126
123
|
);
|
|
127
124
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const normalizedPage = normalizePositiveInteger(pageNo, 'page', 1);
|
|
126
|
+
const searchUrl = buildSogouSearchUrl(normalizedQuery, normalizedPage);
|
|
127
|
+
for (let attempt = 1; attempt <= MAX_SOGOU_SHELL_ATTEMPTS; attempt += 1) {
|
|
128
|
+
const usePreloadedPage = attempt === 1 && preloadedUrl === searchUrl;
|
|
129
|
+
const navigationUrl = attempt === 1
|
|
130
|
+
? searchUrl
|
|
131
|
+
: buildSogouRetryUrl(searchUrl, attempt - 1);
|
|
132
|
+
const payload = await loadSogouPayload(
|
|
133
|
+
page,
|
|
134
|
+
navigationUrl,
|
|
135
|
+
attempt > 1 ? SOGOU_SHELL_RETRY_DELAY_SECONDS : 0,
|
|
136
|
+
!usePreloadedPage,
|
|
135
137
|
);
|
|
138
|
+
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.rows)) {
|
|
139
|
+
throw new CommandExecutionError(
|
|
140
|
+
'weixin sougousearch returned an unreadable browser payload',
|
|
141
|
+
'Sogou Weixin may have changed its result page structure.',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (payload.blocked) {
|
|
145
|
+
throw new AuthRequiredError(
|
|
146
|
+
SOGOU_WEIXIN_DOMAIN,
|
|
147
|
+
'Sogou Weixin requires verification. Complete it in the open browser tab and run the command again.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (payload.invalidCount > 0) {
|
|
151
|
+
throw new CommandExecutionError(
|
|
152
|
+
'Sogou Weixin returned article cards without required title or URL',
|
|
153
|
+
'The result page structure may have changed; refusing to return a partial result set.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (payload.rows.length === 0 && payload.empty) {
|
|
157
|
+
return {
|
|
158
|
+
state: 'empty',
|
|
159
|
+
reason: payload.resultCap ? 'result-cap' : 'no-results',
|
|
160
|
+
page: normalizedPage,
|
|
161
|
+
fingerprint: '',
|
|
162
|
+
rows: [],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (payload.rows.length > 0) {
|
|
166
|
+
return {
|
|
167
|
+
state: 'results',
|
|
168
|
+
page: normalizedPage,
|
|
169
|
+
fingerprint: fingerprintRows(payload.rows),
|
|
170
|
+
rows: payload.rows,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
136
173
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
page
|
|
140
|
-
|
|
141
|
-
rows: payload.rows,
|
|
142
|
-
};
|
|
174
|
+
throw new CommandExecutionError(
|
|
175
|
+
'weixin sougousearch did not expose article result cards',
|
|
176
|
+
'Sogou Weixin returned a transient shell page on both bounded attempts.',
|
|
177
|
+
);
|
|
143
178
|
}
|
package/clis/weixin/articles.js
CHANGED
|
@@ -48,7 +48,7 @@ export const articlesCommand = cli({
|
|
|
48
48
|
if (!accountName) throw withMissingFallbackName('weixin articles', primaryError);
|
|
49
49
|
try {
|
|
50
50
|
const fallback = await collectSogouAccountArticles({
|
|
51
|
-
page, accountName, limit: args.limit, maxPages: args['max-pages'],
|
|
51
|
+
page, accountName, limit: args.limit, maxPages: args['max-pages'], freshPage: true,
|
|
52
52
|
});
|
|
53
53
|
articles = fallback.articles;
|
|
54
54
|
source = fallback.source;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodeFs from 'node:fs';
|
|
2
2
|
import * as nodePath from 'node:path';
|
|
3
3
|
import { cli, Strategy } from '@sovovs/bycli/registry';
|
|
4
|
-
import { ArgumentError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
4
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
|
|
5
5
|
|
|
6
6
|
const WEIXIN_DOMAIN = 'mp.weixin.qq.com';
|
|
7
7
|
const WEIXIN_HOME = 'https://mp.weixin.qq.com/';
|
|
@@ -66,13 +66,19 @@ async function navigateToEditor(page) {
|
|
|
66
66
|
await page.wait(3);
|
|
67
67
|
const token = await getToken(page);
|
|
68
68
|
if (!token) {
|
|
69
|
-
throw new
|
|
69
|
+
throw new AuthRequiredError(
|
|
70
|
+
WEIXIN_DOMAIN,
|
|
71
|
+
'Could not extract session token. Please log in to mp.weixin.qq.com',
|
|
72
|
+
);
|
|
70
73
|
}
|
|
71
74
|
await page.goto(`https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=77&token=${token}&lang=zh_CN`);
|
|
72
75
|
await page.wait(4);
|
|
73
76
|
const hasTitle = await page.evaluate('!!document.querySelector("textarea#title")');
|
|
74
77
|
if (!hasTitle) {
|
|
75
|
-
throw new
|
|
78
|
+
throw new AuthRequiredError(
|
|
79
|
+
WEIXIN_DOMAIN,
|
|
80
|
+
'Article editor did not load. Session may have expired',
|
|
81
|
+
);
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -196,7 +196,7 @@ export const saveArticlesCommand = cli({
|
|
|
196
196
|
try {
|
|
197
197
|
const fallback = await collectSogouAccountArticles({
|
|
198
198
|
page, accountName, limit: args.limit, maxPages: args['max-pages'],
|
|
199
|
-
resolutionPolicy: 'rows',
|
|
199
|
+
resolutionPolicy: 'rows', freshPage: true,
|
|
200
200
|
});
|
|
201
201
|
articles = fallback.articles;
|
|
202
202
|
resolutionFailures = fallback.resolutionFailures;
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 2 Argument / usage error (ArgumentError)
|
|
15
15
|
* 66 No input / empty result (EmptyResultError)
|
|
16
16
|
* 69 Service unavailable (BrowserConnectError, adapter load failures)
|
|
17
|
-
* 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
|
|
17
|
+
* 75 Temporary failure, retry later (TimeoutError, RateLimitedError) EX_TEMPFAIL
|
|
18
18
|
* 77 Permission denied / auth needed (AuthRequiredError)
|
|
19
19
|
* 78 Configuration error (ConfigError)
|
|
20
20
|
* 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
|
|
@@ -51,6 +51,9 @@ export declare class BrowserConnectError extends CliError {
|
|
|
51
51
|
export declare class CommandExecutionError extends CliError {
|
|
52
52
|
constructor(message: string, hint?: string);
|
|
53
53
|
}
|
|
54
|
+
export declare class RateLimitedError extends CliError {
|
|
55
|
+
constructor(message: string, hint?: string);
|
|
56
|
+
}
|
|
54
57
|
export declare class ConfigError extends CliError {
|
|
55
58
|
constructor(message: string, hint?: string);
|
|
56
59
|
}
|
package/dist/src/errors.js
CHANGED
|
@@ -59,6 +59,11 @@ export class CommandExecutionError extends CliError {
|
|
|
59
59
|
super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
export class RateLimitedError extends CliError {
|
|
63
|
+
constructor(message, hint) {
|
|
64
|
+
super('RATE_LIMITED', message, hint, EXIT_CODES.TEMPFAIL);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
62
67
|
export class ConfigError extends CliError {
|
|
63
68
|
constructor(message, hint) {
|
|
64
69
|
super('CONFIG', message, hint, EXIT_CODES.CONFIG_ERROR);
|