From bf5f4705ccc9d6549ab0ed0c55d68c0bade950f3 Mon Sep 17 00:00:00 2001 From: weiyijun Date: Sat, 23 May 2026 09:59:46 +0800 Subject: [PATCH 1/4] feat: add FR SMS channel for step9 phone verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增完全独立的 FR 短信发送渠道,支持用户手动粘贴 phone|verification_url 列表,逐行填写手机号→轮询URL 获取验证码→填写验证码,含调试按钮和内建多轮轮询逻辑。 --- background.js | 13 ++ background/phone-verification-flow.js | 232 ++++++++++++++++++++++++++ phone-sms/providers/fr-sms.js | 211 +++++++++++++++++++++++ phone-sms/providers/registry.js | 8 + sidepanel/sidepanel.html | 33 ++++ sidepanel/sidepanel.js | 166 ++++++++++++++++++ 6 files changed, 663 insertions(+) create mode 100644 phone-sms/providers/fr-sms.js diff --git a/background.js b/background.js index 62bb91a2..fb766127 100644 --- a/background.js +++ b/background.js @@ -20,6 +20,7 @@ importScripts( 'phone-sms/providers/hero-sms.js', 'phone-sms/providers/five-sim.js', 'phone-sms/providers/registry.js', + 'phone-sms/providers/fr-sms.js', 'background/phone-verification-flow.js', 'background/account-run-history.js', 'background/contribution-oauth.js', @@ -1434,6 +1435,10 @@ const PERSISTED_SETTING_DEFAULTS = { nexSmsApiKey: '', nexSmsCountryOrder: [...DEFAULT_NEX_SMS_COUNTRY_ORDER], nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE, + frSmsPhoneList: '', + frSmsPollIntervalSeconds: 3, + frSmsPollTimeoutSeconds: 180, + frSmsOperationDelayMs: 1500, phonePreferredActivation: null, }; @@ -3549,6 +3554,14 @@ function normalizePersistentSettingValue(key, value) { return normalizeNexSmsCountryOrder(value); case 'nexSmsServiceCode': return normalizeNexSmsServiceCode(value); + case 'frSmsPhoneList': + return String(value || ''); + case 'frSmsPollIntervalSeconds': + return Math.max(1, Math.min(60, Number(value) || 3)); + case 'frSmsPollTimeoutSeconds': + return Math.max(10, Math.min(600, Number(value) || 180)); + case 'frSmsOperationDelayMs': + return Math.max(500, Math.min(10000, Number(value) || 1500)); case 'phonePreferredActivation': return normalizePhonePreferredActivation(value); default: diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 08141efd..00bfe786 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -83,11 +83,13 @@ const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO; const PHONE_SMS_PROVIDER_FIVE_SIM = PHONE_SMS_PROVIDER_5SIM; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; + const PHONE_SMS_PROVIDER_FR = 'fr'; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO; const DEFAULT_PHONE_SMS_PROVIDER_ORDER = Object.freeze([ PHONE_SMS_PROVIDER_HERO, PHONE_SMS_PROVIDER_5SIM, PHONE_SMS_PROVIDER_NEXSMS, + PHONE_SMS_PROVIDER_FR, ]); const MAX_PHONE_REUSABLE_POOL = 12; const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; @@ -191,6 +193,9 @@ if (normalized === PHONE_SMS_PROVIDER_NEXSMS) { return PHONE_SMS_PROVIDER_NEXSMS; } + if (normalized === PHONE_SMS_PROVIDER_FR) { + return PHONE_SMS_PROVIDER_FR; + } return PHONE_SMS_PROVIDER_HERO; } function isFiveSimProvider(state = {}) { @@ -935,6 +940,9 @@ if (provider === PHONE_SMS_PROVIDER_NEXSMS) { return 'NexSMS'; } + if (provider === PHONE_SMS_PROVIDER_FR) { + return 'FR'; + } return 'HeroSMS'; } @@ -4624,11 +4632,31 @@ if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_NEXSMS) { return resolveNexSmsCountryCandidates(state); } + if (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FR) { + // FR 渠道使用 HeroSMS 国家配置 + return resolveCountryCandidates(state); + } return resolveCountryCandidates(state); } function resolveCountryConfigFromActivation(activation, fallbackState = {}) { const providerId = getActivationProviderId(activation, fallbackState); + + // FR 渠道:使用 state 中的国家配置或默认值 + if (providerId === PHONE_SMS_PROVIDER_FR) { + const frCountryId = activation?.countryId + || fallbackState?.heroSmsCountryId + || fallbackState?.fiveSimCountryId; + const frCountryLabel = activation?.countryLabel + || fallbackState?.heroSmsCountryLabel + || fallbackState?.fiveSimCountryLabel + || 'Thailand'; + if (frCountryId !== undefined && frCountryId !== null) { + return { id: frCountryId, label: String(frCountryLabel) }; + } + return { id: 52, label: 'Thailand' }; + } + const candidates = resolveCountryCandidatesForProvider(fallbackState, providerId); if (activation && typeof activation === 'object') { if (providerId === PHONE_SMS_PROVIDER_FIVE_SIM) { @@ -6254,10 +6282,201 @@ }); } + // FR 渠道:完全独立的手机验证码流程 + // 从用户粘贴的 phone|url 列表中逐行消费,不依赖任何接码平台 + async function executeFrPhoneVerificationFlow(tabId, state, visibleStep) { + const phoneListText = String(state?.frSmsPhoneList || '').trim(); + if (!phoneListText) { + throw new Error(`步骤 ${visibleStep}:FR 渠道需要先在侧边栏填写号码列表(格式:手机号|验证码获取地址)。`); + } + + const frModule = (typeof self !== 'undefined' ? self : globalThis)?.PhoneSmsFrSmsProvider; + const parseFn = frModule?.parseFrLines || (() => []); + const extractCodeFn = frModule?.extractCode || ((text) => { + const m = String(text || '').match(/\b(\d{4,8})\b/); + return m ? m[1] : ''; + }); + + const allEntries = parseFn(phoneListText); + if (!allEntries.length) { + throw new Error(`步骤 ${visibleStep}:FR 号码列表解析后为空,请检查格式(每行:手机号|验证码URL)。`); + } + + const pollIntervalMs = Math.max(1000, + (Number(state?.frSmsPollIntervalSeconds) || 3) * 1000); + const pollTimeoutMs = Math.max(10000, + (Number(state?.frSmsPollTimeoutSeconds) || 180) * 1000); + const operationDelayMs = Math.max(500, + Number(state?.frSmsOperationDelayMs) || 1500); + const fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null; + if (!fetchImpl) { + throw new Error('FR 渠道需要 fetch API 支持。'); + } + + await addLog( + `步骤 ${visibleStep}:FR 渠道启动,共 ${allEntries.length} 条号码待尝试。轮询间隔 ${pollIntervalMs / 1000}秒,超时 ${pollTimeoutMs / 1000}秒。`, + 'info' + ); + + for (let i = 0; i < allEntries.length; i++) { + throwIfStopped(); + const entry = allEntries[i]; + const phoneNumber = entry.phone; + const codeUrl = entry.url; + + await addLog( + `步骤 ${visibleStep}:FR [${i + 1}/${allEntries.length}] 尝试号码 ${phoneNumber},验证码地址 ${codeUrl}`, + 'info' + ); + + // 1. 填手机号 + try { + await addLog(`步骤 ${visibleStep}:FR 正在填写手机号 ${phoneNumber}...`, 'info'); + const submitResult = await submitPhoneNumber(tabId, phoneNumber, { + phoneNumber, + provider: PHONE_SMS_PROVIDER_FR, + countryId: state?.heroSmsCountryId || 52, + countryLabel: state?.heroSmsCountryLabel || 'Thailand', + }); + if (submitResult?.addPhoneRejected) { + await addLog( + `步骤 ${visibleStep}:FR 号码 ${phoneNumber} 被页面拒绝:${submitResult.errorText || submitResult.url || '未知原因'},尝试下一个。`, + 'warn' + ); + continue; + } + // 操作不要太快 + await sleepWithStop(operationDelayMs); + } catch (error) { + if (isStopRequestedError(error)) throw error; + await addLog( + `步骤 ${visibleStep}:FR 填写手机号 ${phoneNumber} 失败:${error?.message || error},尝试下一个。`, + 'warn' + ); + continue; + } + + // 2. 轮询验证码 + let code = ''; + try { + await addLog( + `步骤 ${visibleStep}:FR 开始轮询验证码 ${codeUrl}(间隔 ${pollIntervalMs / 1000}s,超时 ${pollTimeoutMs / 1000}s)...`, + 'info' + ); + const startTime = Date.now(); + let roundCount = 0; + let lastText = ''; + + while (Date.now() - startTime < pollTimeoutMs) { + throwIfStopped(); + roundCount += 1; + await sleepWithStop(pollIntervalMs); + + try { + const resp = await fetchImpl(codeUrl, { method: 'GET' }); + const text = await resp.text(); + lastText = text; + code = extractCodeFn(text); + + if (code) { + await addLog( + `步骤 ${visibleStep}:FR [第 ${roundCount} 轮] 获取到验证码 ${code}`, + 'ok' + ); + break; + } + + if (frModule?.isNoCodeResponse && frModule.isNoCodeResponse(text)) { + await addLog( + `步骤 ${visibleStep}:FR [第 ${roundCount} 轮] 暂无验证码,${pollIntervalMs / 1000}秒后重试...`, + 'info' + ); + } else { + const preview = text.length > 80 ? `${text.substring(0, 80)}...` : text; + await addLog( + `步骤 ${visibleStep}:FR [第 ${roundCount} 轮] 响应中未提取到验证码:${preview}`, + 'warn' + ); + } + } catch (fetchError) { + lastText = fetchError?.message || String(fetchError); + await addLog( + `步骤 ${visibleStep}:FR [第 ${roundCount} 轮] 请求出错:${lastText}`, + 'warn' + ); + } + } + + if (!code) { + const preview = lastText ? `,最后响应:${lastText.substring(0, 120)}` : ''; + await addLog( + `步骤 ${visibleStep}:FR 号码 ${phoneNumber} 验证码轮询超时${preview},尝试下一个。`, + 'warn' + ); + continue; + } + } catch (error) { + if (isStopRequestedError(error)) throw error; + await addLog( + `步骤 ${visibleStep}:FR 轮询验证码异常:${error?.message || error},尝试下一个。`, + 'warn' + ); + continue; + } + + // 3. 填验证码 + try { + await setPhoneRuntimeState({ + [PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(), + signupPhoneVerificationRequestedAt: Date.now(), + signupPhoneVerificationPurpose: 'login', + }); + await addLog(`步骤 ${visibleStep}:FR 正在提交验证码 ${code}...`, 'info'); + // 操作不要太快 + await sleepWithStop(operationDelayMs); + const submitResult = await submitPhoneVerificationCode(tabId, code); + + if (submitResult?.invalidCode) { + await addLog( + `步骤 ${visibleStep}:FR 验证码 ${code} 无效:${submitResult.errorText || ''},尝试下一个。`, + 'warn' + ); + continue; + } + + await addLog(`步骤 ${visibleStep}:FR 验证码 ${code} 提交成功!`, 'ok'); + // 标记当前条目已使用 + entry.used = true; + return { + code, + phoneNumber, + provider: PHONE_SMS_PROVIDER_FR, + frEntryUsed: true, + }; + } catch (error) { + if (isStopRequestedError(error)) throw error; + await addLog( + `步骤 ${visibleStep}:FR 提交验证码失败:${error?.message || error},尝试下一个。`, + 'warn' + ); + continue; + } + } + + throw new Error(`步骤 ${visibleStep}:FR 渠道已尝试全部 ${allEntries.length} 条号码,均未成功。`); + } + async function completeLoginPhoneVerificationFlow(tabId, options = {}) { const visibleStep = normalizeLogStep(options?.visibleStep || options?.step) || 8; return withPhoneVerificationLogContext({ step: visibleStep, stepKey: 'fetch-login-code' }, async () => { let state = options?.state || await getState(); + + // FR 渠道:完全独立的手机验证流程 + const currentProvider = normalizePhoneSmsProvider(state?.phoneSmsProvider); + if (currentProvider === PHONE_SMS_PROVIDER_FR) { + return executeFrPhoneVerificationFlow(tabId, state, visibleStep); + } + const baseActivation = normalizeActivation( options?.activation || state?.signupPhoneCompletedActivation @@ -6375,6 +6594,19 @@ activePhoneVerificationLogStep = normalizeLogStep(options.visibleStep || options.step) || 9; activePhoneVerificationLogStepKey = 'phone-verification'; let state = await getState(); + + // FR 渠道:完全独立的手机验证流程 + const currentProvider = normalizePhoneSmsProvider(state?.phoneSmsProvider); + if (currentProvider === PHONE_SMS_PROVIDER_FR) { + const visibleStep = activePhoneVerificationLogStep; + try { + return await executeFrPhoneVerificationFlow(tabId, state, visibleStep); + } finally { + activePhoneVerificationLogStep = previousLogStep; + activePhoneVerificationLogStepKey = previousLogStepKey; + } + } + let activation = normalizeActivation(state[PHONE_ACTIVATION_STATE_KEY]); let pageState = initialPageState || await readPhonePageState(tabId); let shouldCancelActivation = false; diff --git a/phone-sms/providers/fr-sms.js b/phone-sms/providers/fr-sms.js new file mode 100644 index 00000000..23c147e4 --- /dev/null +++ b/phone-sms/providers/fr-sms.js @@ -0,0 +1,211 @@ +// phone-sms/providers/fr-sms.js — FR 短信发送渠道(完全独立,不复用其他接码平台代码) +(function attachFrSmsProvider(root, factory) { + root.PhoneSmsFrSmsProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createFrSmsProviderModule() { + const PROVIDER_ID = 'fr'; + const DEFAULT_POLL_INTERVAL_MS = 3000; + const DEFAULT_POLL_TIMEOUT_MS = 180000; + const DEFAULT_REQUEST_TIMEOUT_MS = 15000; + const DEFAULT_OPERATION_DELAY_MS = 1500; + + /** + * 解析用户粘贴的 phone|url 文本,每行一个 + * 格式: 15879103243|http://fr88.site/api/msgForeign?code=14794481d9cdf52b + */ + function parseFrLines(text = '') { + const lines = String(text || '').split(/[\r\n]+/); + const entries = []; + const seenPhones = new Set(); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + const pipeIndex = trimmed.indexOf('|'); + if (pipeIndex < 0) continue; + const phone = trimmed.substring(0, pipeIndex).trim().replace(/[^\d+]/g, ''); + const url = trimmed.substring(pipeIndex + 1).trim(); + if (!phone || !url) continue; + if (seenPhones.has(phone)) continue; + seenPhones.add(phone); + entries.push({ phone, url, used: false }); + } + return entries; + } + + /** + * 从响应文本中提取验证码 + * 响应格式: "您的验证代码是:833831|2026-06-25" 或 "没有获取到验证码|2026-06-25" + */ + function extractCode(text = '') { + const trimmed = String(text || '').trim(); + if (!trimmed) return ''; + // 去掉 | 后面的日期部分,只取消息部分 + const pipeIndex = trimmed.lastIndexOf('|'); + const messagePart = pipeIndex >= 0 ? trimmed.substring(0, pipeIndex) : trimmed; + // 提取 4-8 位数字验证码 + const digitMatch = messagePart.match(/\b(\d{4,8})\b/); + return digitMatch ? digitMatch[1] : ''; + } + + function isNoCodeResponse(text = '') { + const trimmed = String(text || '').trim(); + if (!trimmed) return true; + return /没有获取到验证码|未收到验证码|暂无验证码|no.*verif.*code|not.*found|未收到|暂无短信/i.test(trimmed); + } + + function buildFrLogPrefix(entry = {}, roundCount = 0) { + const phone = String(entry?.phone || '未知号码'); + const masked = phone.length > 4 + ? `${phone.slice(0, 3)}****${phone.slice(-4)}` + : phone; + const roundInfo = roundCount > 0 ? `[第${roundCount}轮]` : ''; + return `FR 渠道 ${masked} ${roundInfo}`.trim(); + } + + async function fetchCodeFromUrl(url = '', fetchImpl, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) { + if (!fetchImpl) { + throw new Error('FR 渠道网络请求不可用'); + } + const controller = typeof AbortController === 'function' ? new AbortController() : null; + const timeoutId = controller + ? setTimeout(() => controller.abort(), requestTimeoutMs) + : null; + try { + const response = await fetchImpl(url, { + method: 'GET', + signal: controller?.signal, + }); + const text = await response.text(); + return { text, ok: response.ok }; + } finally { + if (timeoutId) clearTimeout(timeoutId); + } + } + + async function pollForCode(entry = {}, options = {}, deps = {}) { + const url = String(entry?.url || options?.url || '').trim(); + if (!url) { + throw new Error('FR 渠道缺少验证码获取地址'); + } + const fetchImpl = deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null); + const pollIntervalMs = Math.max(1000, Number(options.pollIntervalMs || deps.pollIntervalMs) || DEFAULT_POLL_INTERVAL_MS); + const pollTimeoutMs = Math.max(5000, Number(options.pollTimeoutMs || deps.pollTimeoutMs) || DEFAULT_POLL_TIMEOUT_MS); + const requestTimeoutMs = Math.max(2000, Number(options.requestTimeoutMs || deps.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS); + const maxRounds = Math.max(0, Number(options.maxRounds || deps.maxRounds) || 0); + const addLog = deps.addLog || options.addLog; + + const start = Date.now(); + let lastText = ''; + let roundCount = 0; + + while (Date.now() - start < pollTimeoutMs) { + if (maxRounds > 0 && roundCount >= maxRounds) break; + + deps.throwIfStopped?.(); + roundCount += 1; + + try { + const { text, ok } = await fetchCodeFromUrl(url, fetchImpl, requestTimeoutMs); + lastText = text; + const code = extractCode(text); + if (code) { + if (typeof addLog === 'function') { + await addLog(`${buildFrLogPrefix(entry, roundCount)} 获取到验证码:${code}`, 'ok'); + } + return { code, raw: text, roundCount, elapsedMs: Date.now() - start }; + } + if (isNoCodeResponse(text)) { + if (typeof addLog === 'function') { + await addLog(`${buildFrLogPrefix(entry, roundCount)} 暂无验证码,${pollIntervalMs / 1000}秒后重试...`, 'info'); + } + } else { + if (typeof addLog === 'function') { + const preview = text.length > 80 ? `${text.substring(0, 80)}...` : text; + await addLog(`${buildFrLogPrefix(entry, roundCount)} 响应中未提取到验证码:${preview}`, 'warn'); + } + } + } catch (error) { + lastText = error?.message || String(error); + if (typeof addLog === 'function') { + await addLog(`${buildFrLogPrefix(entry, roundCount)} 请求出错:${lastText}`, 'warn'); + } + } + + await deps.sleepWithStop?.(pollIntervalMs); + } + + const timeoutSeconds = Math.ceil(pollTimeoutMs / 1000); + const preview = lastText ? `,最后响应:${lastText.substring(0, 120)}` : ''; + throw new Error(`FR 渠道获取验证码超时(${timeoutSeconds}秒/${roundCount}轮)${preview}`); + } + + /** 调试用:直接请求指定 URL 并打印返回的验证码 */ + async function debugFetchCode(url = '', options = {}, deps = {}) { + const fetchImpl = deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null); + if (!fetchImpl) { + return { url, code: '', error: '网络请求不可用' }; + } + try { + const { text, ok } = await fetchCodeFromUrl(url, fetchImpl, + Number(options.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS); + const code = extractCode(text); + return { url, code, raw: text, ok }; + } catch (error) { + return { url, code: '', error: error?.message || String(error) }; + } + } + + function normalizeFrPollIntervalSeconds(state = {}) { + const seconds = Math.max(1, Math.min(60, Number(state?.frPollIntervalSeconds) || 3)); + return seconds; + } + + function normalizeFrPollTimeoutSeconds(state = {}) { + const seconds = Math.max(10, Math.min(600, Number(state?.frPollTimeoutSeconds) || 180)); + return seconds; + } + + function normalizeFrOperationDelayMs(state = {}) { + const ms = Math.max(500, Math.min(10000, Number(state?.frOperationDelayMs) || DEFAULT_OPERATION_DELAY_MS)); + return ms; + } + + function createProvider(deps = {}) { + const providerDeps = { + fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null), + sleepWithStop: deps.sleepWithStop, + throwIfStopped: deps.throwIfStopped, + addLog: deps.addLog, + requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, + pollIntervalMs: deps.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS, + pollTimeoutMs: deps.pollTimeoutMs || DEFAULT_POLL_TIMEOUT_MS, + }; + return { + id: PROVIDER_ID, + label: 'FR', + parseLines: parseFrLines, + extractCode, + isNoCodeResponse, + fetchCodeFromUrl: (url) => fetchCodeFromUrl(url, providerDeps.fetchImpl), + pollForCode: (entry, options) => pollForCode(entry, options, providerDeps), + debugFetchCode: (url, options) => debugFetchCode(url, options, providerDeps), + }; + } + + return { + PROVIDER_ID, + DEFAULT_POLL_INTERVAL_MS, + DEFAULT_POLL_TIMEOUT_MS, + DEFAULT_REQUEST_TIMEOUT_MS, + DEFAULT_OPERATION_DELAY_MS, + createProvider, + parseFrLines, + extractCode, + isNoCodeResponse, + fetchCodeFromUrl, + pollForCode, + debugFetchCode, + normalizeFrPollIntervalSeconds, + normalizeFrPollTimeoutSeconds, + normalizeFrOperationDelayMs, + }; +}); diff --git a/phone-sms/providers/registry.js b/phone-sms/providers/registry.js index 070da152..1d296982 100644 --- a/phone-sms/providers/registry.js +++ b/phone-sms/providers/registry.js @@ -5,11 +5,13 @@ const PROVIDER_HERO_SMS = 'hero-sms'; const PROVIDER_FIVE_SIM = '5sim'; const PROVIDER_NEXSMS = 'nexsms'; + const PROVIDER_FR_SMS = 'fr'; const DEFAULT_PROVIDER = PROVIDER_HERO_SMS; const DEFAULT_PROVIDER_ORDER = Object.freeze([ PROVIDER_HERO_SMS, PROVIDER_FIVE_SIM, PROVIDER_NEXSMS, + PROVIDER_FR_SMS, ]); const PROVIDER_DEFINITIONS = Object.freeze({ [PROVIDER_HERO_SMS]: Object.freeze({ @@ -27,6 +29,11 @@ label: 'NexSMS', moduleKey: 'PhoneSmsNexSmsProvider', }), + [PROVIDER_FR_SMS]: Object.freeze({ + id: PROVIDER_FR_SMS, + label: 'FR', + moduleKey: 'PhoneSmsFrSmsProvider', + }), }); function resolveProviderKey(value = '') { @@ -125,6 +132,7 @@ PROVIDER_HERO_SMS, PROVIDER_FIVE_SIM, PROVIDER_NEXSMS, + PROVIDER_FR_SMS, DEFAULT_PROVIDER, DEFAULT_PROVIDER_ORDER, PROVIDER_DEFINITIONS, diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index ccc80e7b..80aba523 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -1436,6 +1436,7 @@ +