feat(phone-sms): support 5sim provider
(cherry picked from commit 7d10cab9765ed662b5088f791c8635fd8cc63e44)
This commit is contained in:
+96
-231
@@ -4,6 +4,9 @@ importScripts(
|
||||
'managed-alias-utils.js',
|
||||
'mail2925-utils.js',
|
||||
'paypal-utils.js',
|
||||
'phone-sms/providers/hero-sms.js',
|
||||
'phone-sms/providers/five-sim.js',
|
||||
'phone-sms/providers/registry.js',
|
||||
'background/phone-verification-flow.js',
|
||||
'background/account-run-history.js',
|
||||
'background/contribution-oauth.js',
|
||||
@@ -338,6 +341,12 @@ const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE_HIGH = 'price_high';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const FIVE_SIM_OPERATOR = 'any';
|
||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||||
const PERSISTENT_ALIAS_STATE_KEYS = [
|
||||
@@ -565,6 +574,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
hotmailAccounts: [],
|
||||
mail2925Accounts: [],
|
||||
paypalAccounts: [],
|
||||
phoneSmsProvider: DEFAULT_PHONE_SMS_PROVIDER,
|
||||
heroSmsApiKey: '',
|
||||
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
|
||||
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
|
||||
@@ -573,16 +583,12 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
|
||||
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
|
||||
heroSmsCountryFallback: [],
|
||||
phonePreferredActivation: null,
|
||||
fiveSimApiKey: '',
|
||||
fiveSimBaseUrl: DEFAULT_FIVE_SIM_BASE_URL,
|
||||
fiveSimCountryOrder: [],
|
||||
fiveSimOperator: DEFAULT_FIVE_SIM_OPERATOR,
|
||||
fiveSimProduct: DEFAULT_FIVE_SIM_PRODUCT,
|
||||
nexSmsApiKey: '',
|
||||
nexSmsBaseUrl: DEFAULT_NEX_SMS_BASE_URL,
|
||||
nexSmsCountryOrder: [],
|
||||
nexSmsServiceCode: DEFAULT_NEX_SMS_SERVICE_CODE,
|
||||
fiveSimCountryId: FIVE_SIM_COUNTRY_ID,
|
||||
fiveSimCountryLabel: FIVE_SIM_COUNTRY_LABEL,
|
||||
fiveSimCountryFallback: [],
|
||||
fiveSimMaxPrice: '',
|
||||
fiveSimOperator: FIVE_SIM_OPERATOR,
|
||||
};
|
||||
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
@@ -944,156 +950,99 @@ function normalizeHeroSmsCountryFallback(value = []) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizePhonePreferredActivation(value = null) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = normalizePhoneSmsProvider(value.provider || '');
|
||||
const activationId = String(value.activationId ?? value.id ?? value.activation ?? '').trim();
|
||||
const phoneNumber = String(value.phoneNumber ?? value.phone ?? value.number ?? '').trim();
|
||||
if (!activationId || !phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {
|
||||
provider,
|
||||
activationId,
|
||||
phoneNumber,
|
||||
successfulUses: Math.max(0, Math.floor(Number(value.successfulUses) || 0)),
|
||||
maxUses: Math.max(1, Math.floor(Number(value.maxUses) || 3)),
|
||||
};
|
||||
|
||||
if (provider === PHONE_SMS_PROVIDER_5SIM) {
|
||||
const countryCode = normalizeFiveSimCountryCode(value.countryCode || value.countryId || '', '');
|
||||
if (countryCode) {
|
||||
normalized.countryId = countryCode;
|
||||
normalized.countryCode = countryCode;
|
||||
}
|
||||
} else if (provider === PHONE_SMS_PROVIDER_NEXSMS) {
|
||||
const countryId = normalizeNexSmsCountryId(value.countryId, -1);
|
||||
if (countryId >= 0) {
|
||||
normalized.countryId = countryId;
|
||||
}
|
||||
} else {
|
||||
const countryId = Math.floor(Number(value.countryId) || 0);
|
||||
if (countryId > 0) {
|
||||
normalized.countryId = countryId;
|
||||
}
|
||||
}
|
||||
|
||||
const countryLabel = String(value.countryLabel || '').trim();
|
||||
if (countryLabel) {
|
||||
normalized.countryLabel = countryLabel;
|
||||
}
|
||||
const serviceCode = String(value.serviceCode || '').trim();
|
||||
if (serviceCode) {
|
||||
normalized.serviceCode = serviceCode;
|
||||
}
|
||||
const expiresAt = Number(value.expiresAt);
|
||||
if (Number.isFinite(expiresAt) && expiresAt > 0) {
|
||||
normalized.expiresAt = Math.floor(expiresAt);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizePhoneSmsProvider(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) {
|
||||
return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PHONE_SMS_PROVIDER_5SIM) {
|
||||
return PHONE_SMS_PROVIDER_5SIM;
|
||||
}
|
||||
if (normalized === PHONE_SMS_PROVIDER_NEXSMS) {
|
||||
return PHONE_SMS_PROVIDER_NEXSMS;
|
||||
}
|
||||
return PHONE_SMS_PROVIDER_HERO;
|
||||
return normalized === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
: PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
}
|
||||
|
||||
function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;|/]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
|
||||
source.forEach((entry) => {
|
||||
const provider = normalizePhoneSmsProvider(entry);
|
||||
if (seen.has(provider)) {
|
||||
return;
|
||||
}
|
||||
seen.add(provider);
|
||||
normalized.push(provider);
|
||||
});
|
||||
|
||||
if (normalized.length) {
|
||||
return normalized.slice(0, 3);
|
||||
function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryId(value, fallback);
|
||||
}
|
||||
|
||||
const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : [];
|
||||
const fallbackNormalized = [];
|
||||
fallback.forEach((providerEntry) => {
|
||||
const provider = normalizePhoneSmsProvider(providerEntry);
|
||||
if (!provider || fallbackNormalized.includes(provider)) {
|
||||
return;
|
||||
}
|
||||
fallbackNormalized.push(provider);
|
||||
});
|
||||
|
||||
return fallbackNormalized.slice(0, 3);
|
||||
}
|
||||
|
||||
function normalizeFiveSimBaseUrl(value = '', fallback = DEFAULT_FIVE_SIM_BASE_URL) {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return fallback;
|
||||
}
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryCode(value = '', fallback = 'thailand') {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, '');
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryOrder(value = []) {
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = FIVE_SIM_COUNTRY_LABEL) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryLabel) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryLabel(value, fallback);
|
||||
}
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.formatFiveSimCountryLabel) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.formatFiveSimCountryLabel('', value, fallback);
|
||||
}
|
||||
return String(value || '').trim() || fallback;
|
||||
}
|
||||
|
||||
function normalizeFiveSimOperator(value = '', fallback = FIVE_SIM_OPERATOR) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimOperator) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimOperator(value || fallback);
|
||||
}
|
||||
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback;
|
||||
}
|
||||
|
||||
function normalizeFiveSimMaxPrice(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimMaxPrice) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimMaxPrice(value);
|
||||
}
|
||||
const rawValue = String(value ?? '').trim();
|
||||
if (!rawValue) {
|
||||
return '';
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return '';
|
||||
}
|
||||
return String(Math.round(numeric * 10000) / 10000);
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryFallback(value = []) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryFallback) {
|
||||
return rootScope.PhoneSmsFiveSimProvider.normalizeFiveSimCountryFallback(value);
|
||||
}
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const seen = new Set();
|
||||
const seenIds = new Set();
|
||||
const normalized = [];
|
||||
|
||||
for (const entry of source) {
|
||||
let code = '';
|
||||
let countryId = '';
|
||||
let countryLabel = '';
|
||||
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
code = normalizeFiveSimCountryCode(entry.code || entry.country || entry.id || '', '');
|
||||
countryId = normalizeFiveSimCountryId(entry.countryId ?? entry.id ?? entry.slug, '');
|
||||
countryLabel = String((entry.countryLabel ?? entry.label ?? entry.name ?? entry.text_en) || '').trim();
|
||||
} else {
|
||||
code = normalizeFiveSimCountryCode(entry, '');
|
||||
const text = String(entry || '').trim();
|
||||
const structuredMatch = text.match(/^([a-z0-9_-]+)\s*(?:[:|/-]\s*(.+))?$/i);
|
||||
countryId = normalizeFiveSimCountryId(structuredMatch?.[1] || text, '');
|
||||
countryLabel = String(structuredMatch?.[2] || '').trim();
|
||||
}
|
||||
|
||||
if (!code || seen.has(code)) {
|
||||
if (!countryId || seenIds.has(countryId)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(code);
|
||||
normalized.push(code);
|
||||
if (normalized.length >= 10) {
|
||||
seenIds.add(countryId);
|
||||
normalized.push({
|
||||
id: countryId,
|
||||
label: countryLabel || normalizeFiveSimCountryLabel('', countryId),
|
||||
});
|
||||
if (normalized.length >= 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1101,85 +1050,6 @@ function normalizeFiveSimCountryOrder(value = []) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeFiveSimOperator(value = '', fallback = DEFAULT_FIVE_SIM_OPERATOR) {
|
||||
return normalizeFiveSimCountryCode(value, fallback);
|
||||
}
|
||||
|
||||
function normalizeFiveSimProduct(value = '', fallback = DEFAULT_FIVE_SIM_PRODUCT) {
|
||||
return normalizeFiveSimCountryCode(value, fallback);
|
||||
}
|
||||
|
||||
function normalizeNexSmsBaseUrl(value = '', fallback = DEFAULT_NEX_SMS_BASE_URL) {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return fallback;
|
||||
}
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeNexSmsCountryId(value, fallback = 0) {
|
||||
const parsed = Math.floor(Number(value));
|
||||
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||
return parsed;
|
||||
}
|
||||
const fallbackParsed = Math.floor(Number(fallback));
|
||||
if (Number.isFinite(fallbackParsed) && fallbackParsed >= 0) {
|
||||
return fallbackParsed;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function normalizeNexSmsCountryOrder(value = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
for (const entry of source) {
|
||||
const countryId = normalizeNexSmsCountryId(
|
||||
entry && typeof entry === 'object' && !Array.isArray(entry)
|
||||
? (entry.id || entry.countryId || entry.country || '')
|
||||
: entry,
|
||||
-1
|
||||
);
|
||||
if (countryId < 0 || seen.has(countryId)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(countryId);
|
||||
normalized.push(countryId);
|
||||
if (normalized.length >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_NEX_SMS_SERVICE_CODE) {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, '');
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
const fallbackNormalized = String(fallback || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, '');
|
||||
return fallbackNormalized || 'ot';
|
||||
}
|
||||
|
||||
function resolveLegacyAutoStepDelaySeconds(input = {}) {
|
||||
const hasLegacyMin = input.autoStepRandomDelayMinSeconds !== undefined;
|
||||
const hasLegacyMax = input.autoStepRandomDelayMaxSeconds !== undefined;
|
||||
@@ -2040,6 +1910,8 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeMail2925Accounts(value);
|
||||
case 'paypalAccounts':
|
||||
return normalizePayPalAccounts(value);
|
||||
case 'phoneSmsProvider':
|
||||
return normalizePhoneSmsProvider(value);
|
||||
case 'heroSmsApiKey':
|
||||
return String(value || '');
|
||||
case 'heroSmsReuseEnabled':
|
||||
@@ -2056,26 +1928,18 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || HERO_SMS_COUNTRY_LABEL).trim() || HERO_SMS_COUNTRY_LABEL;
|
||||
case 'heroSmsCountryFallback':
|
||||
return normalizeHeroSmsCountryFallback(value);
|
||||
case 'phonePreferredActivation':
|
||||
return normalizePhonePreferredActivation(value);
|
||||
case 'fiveSimApiKey':
|
||||
return String(value || '').trim();
|
||||
case 'fiveSimBaseUrl':
|
||||
return normalizeFiveSimBaseUrl(value, DEFAULT_FIVE_SIM_BASE_URL);
|
||||
case 'fiveSimCountryOrder':
|
||||
return normalizeFiveSimCountryOrder(value);
|
||||
return String(value || '');
|
||||
case 'fiveSimCountryId':
|
||||
return normalizeFiveSimCountryId(value);
|
||||
case 'fiveSimCountryLabel':
|
||||
return normalizeFiveSimCountryLabel(value);
|
||||
case 'fiveSimCountryFallback':
|
||||
return normalizeFiveSimCountryFallback(value);
|
||||
case 'fiveSimMaxPrice':
|
||||
return normalizeFiveSimMaxPrice(value);
|
||||
case 'fiveSimOperator':
|
||||
return normalizeFiveSimOperator(value, DEFAULT_FIVE_SIM_OPERATOR);
|
||||
case 'fiveSimProduct':
|
||||
return normalizeFiveSimProduct(value, DEFAULT_FIVE_SIM_PRODUCT);
|
||||
case 'nexSmsApiKey':
|
||||
return String(value || '').trim();
|
||||
case 'nexSmsBaseUrl':
|
||||
return normalizeNexSmsBaseUrl(value, DEFAULT_NEX_SMS_BASE_URL);
|
||||
case 'nexSmsCountryOrder':
|
||||
return normalizeNexSmsCountryOrder(value);
|
||||
case 'nexSmsServiceCode':
|
||||
return normalizeNexSmsServiceCode(value, DEFAULT_NEX_SMS_SERVICE_CODE);
|
||||
return normalizeFiveSimOperator(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
@@ -9693,6 +9557,7 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
createFiveSimProvider: self.PhoneSmsFiveSimProvider?.createProvider,
|
||||
});
|
||||
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||||
addLog,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,8 @@
|
||||
4. Plus 免费试用资格提前判断,以及跳过账号仍标记“已用”。
|
||||
5. SUB2API 多分组创建账号。
|
||||
6. OAuth 登录使用全新标签页。
|
||||
7. 本地导出配置 `/config.json` 忽略规则。
|
||||
7. 接码平台多平台适配:HeroSMS + 5sim。
|
||||
8. 本地导出配置 `/config.json` 忽略规则。
|
||||
|
||||
推荐每次同步后执行:
|
||||
|
||||
@@ -37,6 +38,8 @@ cc7671e feat(sub2api): 支持多分组创建账号
|
||||
b3790c1 fix(auth): OAuth 登录使用全新标签页
|
||||
```
|
||||
|
||||
补充:接码平台多平台适配是当前分支新增的本地自定义功能;真实 5sim API Key 只允许通过侧边栏输入并保存到本地配置,不写入仓库、测试或文档。
|
||||
|
||||
排查本地定制范围可用:
|
||||
|
||||
```bash
|
||||
@@ -304,7 +307,71 @@ OAuth 登录流程打开新标签页,避免复用旧页面导致状态污染
|
||||
|
||||
上游如果改 OAuth tab 打开方式,确认仍保留“全新标签页”语义,不要退回到复用已有 tab。
|
||||
|
||||
## 7. 本地配置文件忽略
|
||||
## 7. 接码平台多平台适配:HeroSMS + 5sim
|
||||
|
||||
### 目标
|
||||
|
||||
手机号验证 Step 9 支持按平台切换接码能力:
|
||||
|
||||
- 默认仍为 `HeroSMS`,老配置无需迁移。
|
||||
- 新增 `5sim` 平台,下拉框在接码设置卡片中直接可见。
|
||||
- 国家/地区列表、API Key、价格上限、余额、买号、查码、完成、取消、ban、复用均按当前 `phoneSmsProvider` 分发。
|
||||
- HeroSMS 继续使用原 `heroSms*` 字段;5sim 使用独立 `fiveSim*` 字段,切换平台时不会互相覆盖草稿。
|
||||
|
||||
### 关键文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| `phone-sms/providers/hero-sms.js` | HeroSMS 适配层,承接 HeroSMS 余额、价格、国家规范化等平台逻辑。 |
|
||||
| `phone-sms/providers/five-sim.js` | 5sim 适配层,封装 profile、countries、prices、buy、check、finish、cancel、ban、reuse。 |
|
||||
| `phone-sms/providers/registry.js` | 接码平台注册表,按 `phoneSmsProvider` 选择平台模块。 |
|
||||
| `background.js` | 持久字段默认值、导入脚本、配置规范化、phone flow helper 注入。 |
|
||||
| `background/phone-verification-flow.js` | Step 9 运行态按 activation.provider 分发后续查码/完成/取消/ban/reuse。 |
|
||||
| `sidepanel/sidepanel.html` | 接码平台下拉、余额按钮、5sim operator 输入。 |
|
||||
| `sidepanel/sidepanel.js` | 平台切换、地区列表联动、价格/余额查询、配置收集与恢复。 |
|
||||
| `tests/five-sim-provider.test.js` | 5sim provider 单测。 |
|
||||
| `tests/phone-verification-flow.test.js` | 5sim Step 9 buy/check/finish/reuse 分发测试。 |
|
||||
| `tests/sidepanel-phone-verification-settings.test.js` | 接码平台下拉和侧边栏联动测试。 |
|
||||
|
||||
### 关键状态字段
|
||||
|
||||
- 通用:
|
||||
- `phoneSmsProvider: "hero-sms" | "5sim"`
|
||||
- HeroSMS:
|
||||
- `heroSmsApiKey`
|
||||
- `heroSmsCountryId`
|
||||
- `heroSmsCountryLabel`
|
||||
- `heroSmsCountryFallback`
|
||||
- `heroSmsMaxPrice`
|
||||
- `heroSmsReuseEnabled`
|
||||
- `heroSmsAcquirePriority`
|
||||
- 5sim:
|
||||
- `fiveSimApiKey`
|
||||
- `fiveSimCountryId`
|
||||
- `fiveSimCountryLabel`
|
||||
- `fiveSimCountryFallback`
|
||||
- `fiveSimMaxPrice`
|
||||
- `fiveSimOperator`
|
||||
|
||||
### 维护注意
|
||||
|
||||
1. 不要把真实 5sim key 写入代码、测试或文档;只允许用户在侧边栏输入。
|
||||
2. 上游同步时如果改了 Step 9 手机号验证,必须保留 `currentPhoneActivation.provider` 驱动的后续分发。
|
||||
3. 上游同步时如果改了 sidepanel 接码区域,必须保留 `select-phone-sms-provider`,且地区列表按平台加载:HeroSMS 数字国家 ID,5sim 字符串 country slug。
|
||||
4. 5sim 产品码当前固定为 `openai`,operator 默认 `any`;价格上限为空时按价格目录最低可用价尝试。
|
||||
|
||||
### 验证
|
||||
|
||||
```bash
|
||||
node --check phone-sms/providers/hero-sms.js
|
||||
node --check phone-sms/providers/five-sim.js
|
||||
node --check phone-sms/providers/registry.js
|
||||
node --check background/phone-verification-flow.js
|
||||
node --check sidepanel/sidepanel.js
|
||||
node --test tests/five-sim-provider.test.js tests/phone-verification-flow.test.js tests/sidepanel-phone-verification-settings.test.js
|
||||
```
|
||||
|
||||
## 8. 本地配置文件忽略
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -388,3 +455,13 @@ rg -n "normalizeSub2ApiGroupNames|getGroupsByNames|sub2apiGroupIds|input-sub2api
|
||||
tests
|
||||
```
|
||||
|
||||
检查接码多平台适配:
|
||||
|
||||
```bash
|
||||
rg -n "phoneSmsProvider|select-phone-sms-provider|PhoneSmsFiveSimProvider|PhoneSmsProviderRegistry|fiveSim" \
|
||||
background.js \
|
||||
background/phone-verification-flow.js \
|
||||
phone-sms \
|
||||
sidepanel \
|
||||
tests
|
||||
```
|
||||
|
||||
@@ -0,0 +1,822 @@
|
||||
// phone-sms/providers/five-sim.js — 5sim 接码平台适配层
|
||||
(function attachFiveSimSmsProvider(root, factory) {
|
||||
root.PhoneSmsFiveSimProvider = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createFiveSimSmsProviderModule() {
|
||||
const PROVIDER_ID = '5sim';
|
||||
const DEFAULT_BASE_URL = 'https://5sim.net';
|
||||
const DEFAULT_PRODUCT = 'openai';
|
||||
const DEFAULT_OPERATOR = 'any';
|
||||
const DEFAULT_COUNTRY_ID = 'england';
|
||||
const DEFAULT_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||||
const DEFAULT_MAX_USES = 3;
|
||||
const MAX_PRICE_CANDIDATES = 8;
|
||||
const COUNTRY_CN_BY_ID = Object.freeze({
|
||||
afghanistan: '阿富汗',
|
||||
albania: '阿尔巴尼亚',
|
||||
algeria: '阿尔及利亚',
|
||||
angola: '安哥拉',
|
||||
argentina: '阿根廷',
|
||||
armenia: '亚美尼亚',
|
||||
australia: '澳大利亚',
|
||||
austria: '奥地利',
|
||||
azerbaijan: '阿塞拜疆',
|
||||
bahamas: '巴哈马',
|
||||
bahrain: '巴林',
|
||||
bangladesh: '孟加拉国',
|
||||
belarus: '白俄罗斯',
|
||||
belgium: '比利时',
|
||||
bolivia: '玻利维亚',
|
||||
bosnia: '波黑',
|
||||
brazil: '巴西',
|
||||
bulgaria: '保加利亚',
|
||||
cambodia: '柬埔寨',
|
||||
cameroon: '喀麦隆',
|
||||
canada: '加拿大',
|
||||
chile: '智利',
|
||||
china: '中国',
|
||||
colombia: '哥伦比亚',
|
||||
croatia: '克罗地亚',
|
||||
cyprus: '塞浦路斯',
|
||||
czech: '捷克',
|
||||
denmark: '丹麦',
|
||||
egypt: '埃及',
|
||||
england: '英国',
|
||||
estonia: '爱沙尼亚',
|
||||
ethiopia: '埃塞俄比亚',
|
||||
finland: '芬兰',
|
||||
france: '法国',
|
||||
georgia: '格鲁吉亚',
|
||||
germany: '德国',
|
||||
ghana: '加纳',
|
||||
greece: '希腊',
|
||||
hongkong: '中国香港',
|
||||
hungary: '匈牙利',
|
||||
india: '印度',
|
||||
indonesia: '印度尼西亚',
|
||||
ireland: '爱尔兰',
|
||||
israel: '以色列',
|
||||
italy: '意大利',
|
||||
japan: '日本',
|
||||
jordan: '约旦',
|
||||
kazakhstan: '哈萨克斯坦',
|
||||
kenya: '肯尼亚',
|
||||
kyrgyzstan: '吉尔吉斯斯坦',
|
||||
laos: '老挝',
|
||||
latvia: '拉脱维亚',
|
||||
lithuania: '立陶宛',
|
||||
malaysia: '马来西亚',
|
||||
mexico: '墨西哥',
|
||||
moldova: '摩尔多瓦',
|
||||
morocco: '摩洛哥',
|
||||
myanmar: '缅甸',
|
||||
nepal: '尼泊尔',
|
||||
netherlands: '荷兰',
|
||||
newzealand: '新西兰',
|
||||
nigeria: '尼日利亚',
|
||||
norway: '挪威',
|
||||
pakistan: '巴基斯坦',
|
||||
paraguay: '巴拉圭',
|
||||
peru: '秘鲁',
|
||||
philippines: '菲律宾',
|
||||
poland: '波兰',
|
||||
portugal: '葡萄牙',
|
||||
romania: '罗马尼亚',
|
||||
russia: '俄罗斯',
|
||||
saudiarabia: '沙特阿拉伯',
|
||||
serbia: '塞尔维亚',
|
||||
singapore: '新加坡',
|
||||
slovakia: '斯洛伐克',
|
||||
slovenia: '斯洛文尼亚',
|
||||
southafrica: '南非',
|
||||
spain: '西班牙',
|
||||
srilanka: '斯里兰卡',
|
||||
sweden: '瑞典',
|
||||
switzerland: '瑞士',
|
||||
taiwan: '中国台湾',
|
||||
tajikistan: '塔吉克斯坦',
|
||||
tanzania: '坦桑尼亚',
|
||||
thailand: '泰国',
|
||||
turkey: '土耳其',
|
||||
ukraine: '乌克兰',
|
||||
uruguay: '乌拉圭',
|
||||
usa: '美国',
|
||||
uzbekistan: '乌兹别克斯坦',
|
||||
venezuela: '委内瑞拉',
|
||||
vietnam: '越南',
|
||||
});
|
||||
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_COUNTRY_ID) {
|
||||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function getCountryIdFromPayload(record = {}, fallback = DEFAULT_COUNTRY_ID) {
|
||||
if (record?.country && typeof record.country === 'object' && !Array.isArray(record.country)) {
|
||||
return normalizeFiveSimCountryId(record.country.name || record.country.id || record.country.slug, fallback);
|
||||
}
|
||||
return normalizeFiveSimCountryId(record.countryId ?? record.country, fallback);
|
||||
}
|
||||
|
||||
function getCountryLabelFromPayload(record = {}, fallbackLabel = DEFAULT_COUNTRY_LABEL, fallbackId = DEFAULT_COUNTRY_ID) {
|
||||
if (record?.country && typeof record.country === 'object' && !Array.isArray(record.country)) {
|
||||
const countryId = normalizeFiveSimCountryId(record.country.name || record.country.id || record.country.slug || fallbackId, fallbackId);
|
||||
return formatFiveSimCountryLabel(countryId, record.country.text_en || record.country.label || countryId, fallbackLabel || fallbackId);
|
||||
}
|
||||
const countryId = normalizeFiveSimCountryId(record.countryId ?? record.country ?? fallbackId, fallbackId);
|
||||
return normalizeFiveSimCountryLabel(record.countryLabel, formatFiveSimCountryLabel(countryId, countryId, fallbackLabel || fallbackId));
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||
return String(value || '').trim() || fallback;
|
||||
}
|
||||
|
||||
function formatFiveSimCountryLabel(id = '', englishValue = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||
const countryId = normalizeFiveSimCountryId(id, '');
|
||||
const english = normalizeFiveSimCountryLabel(englishValue || countryId || fallback, fallback);
|
||||
const chinese = COUNTRY_CN_BY_ID[countryId] || '';
|
||||
if (chinese && english && chinese.toLowerCase() !== english.toLowerCase() && !String(english).includes(chinese)) {
|
||||
return `${chinese} (${english})`;
|
||||
}
|
||||
return chinese || english;
|
||||
}
|
||||
|
||||
function normalizeFiveSimOperator(value = '') {
|
||||
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_OPERATOR;
|
||||
}
|
||||
|
||||
function normalizeFiveSimMaxPrice(value = '') {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
if (!rawValue) {
|
||||
return '';
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return '';
|
||||
}
|
||||
return String(Math.round(numeric * 10000) / 10000);
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryFallback(value = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
|
||||
for (const entry of source) {
|
||||
let id = '';
|
||||
let label = '';
|
||||
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
id = normalizeFiveSimCountryId(entry.id ?? entry.countryId ?? entry.slug, '');
|
||||
label = String((entry.label ?? entry.countryLabel ?? entry.text_en ?? entry.name) || '').trim();
|
||||
} else {
|
||||
const text = String(entry || '').trim();
|
||||
const structured = text.match(/^([a-z0-9_-]+)\s*(?:[:|/-]\s*(.+))?$/i);
|
||||
id = normalizeFiveSimCountryId(structured?.[1] || text, '');
|
||||
label = String(structured?.[2] || '').trim();
|
||||
}
|
||||
|
||||
if (!id || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
normalized.push({
|
||||
id,
|
||||
label: label || formatFiveSimCountryLabel(id, id, id),
|
||||
});
|
||||
if (normalized.length >= 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizePrice(value) {
|
||||
const price = Number(value);
|
||||
if (!Number.isFinite(price) || price < 0) {
|
||||
return null;
|
||||
}
|
||||
return price;
|
||||
}
|
||||
|
||||
function buildSortedUniquePriceCandidates(values = []) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((value) => normalizePrice(value))
|
||||
.filter((value) => value !== null)
|
||||
.map((value) => Math.round(value * 10000) / 10000)
|
||||
)
|
||||
)
|
||||
.sort((left, right) => left - right)
|
||||
.slice(0, MAX_PRICE_CANDIDATES);
|
||||
}
|
||||
|
||||
function describePayload(raw) {
|
||||
if (typeof raw === 'string') {
|
||||
return raw.trim();
|
||||
}
|
||||
if (raw && typeof raw === 'object') {
|
||||
const direct = String(raw.message || raw.msg || raw.error || raw.title || raw.status || '').trim();
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(raw);
|
||||
} catch {
|
||||
return String(raw);
|
||||
}
|
||||
}
|
||||
return String(raw || '').trim();
|
||||
}
|
||||
|
||||
function parsePayload(text) {
|
||||
const trimmed = String(text || '').trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value = '') {
|
||||
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
return `${url.origin}${url.pathname.replace(/\/+$/, '')}`;
|
||||
} catch {
|
||||
return DEFAULT_BASE_URL;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(config = {}, path = '', query = {}) {
|
||||
const url = new URL(path, `${normalizeBaseUrl(config.baseUrl)}/`);
|
||||
Object.entries(query || {}).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return;
|
||||
}
|
||||
url.searchParams.set(key, String(value));
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function fetchJson(config = {}, path = '', options = {}) {
|
||||
const { query = {}, actionLabel = '5sim 请求', requireAuth = true } = options;
|
||||
const token = String(config.apiKey || '').trim();
|
||||
if (requireAuth && !token) {
|
||||
throw new Error('缺少 5sim API Key,请先在侧边栏接码设置中填写并保存。');
|
||||
}
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||
const timeoutId = controller
|
||||
? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS)
|
||||
: null;
|
||||
|
||||
try {
|
||||
const response = await config.fetchImpl(buildUrl(config, path, query), {
|
||||
method: 'GET',
|
||||
signal: controller?.signal,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(requireAuth ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
const text = await response.text();
|
||||
const payload = parsePayload(text);
|
||||
if (!response.ok) {
|
||||
const error = new Error(`${actionLabel}失败:${describePayload(payload) || response.status}`);
|
||||
error.payload = payload;
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`${actionLabel}超时。`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveConfig(state = {}, deps = {}) {
|
||||
return {
|
||||
apiKey: String(state.fiveSimApiKey || '').trim(),
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCountryConfig(state = {}) {
|
||||
return {
|
||||
id: normalizeFiveSimCountryId(state.fiveSimCountryId),
|
||||
label: normalizeFiveSimCountryLabel(state.fiveSimCountryLabel),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCountryCandidates(state = {}) {
|
||||
const primary = resolveCountryConfig(state);
|
||||
const fallbackList = normalizeFiveSimCountryFallback(state.fiveSimCountryFallback);
|
||||
const seen = new Set([primary.id]);
|
||||
const candidates = [primary];
|
||||
|
||||
fallbackList.forEach((entry) => {
|
||||
const nextId = normalizeFiveSimCountryId(entry.id, '');
|
||||
if (!nextId || seen.has(nextId)) {
|
||||
return;
|
||||
}
|
||||
seen.add(nextId);
|
||||
candidates.push({
|
||||
id: nextId,
|
||||
label: normalizeFiveSimCountryLabel(entry.label, nextId),
|
||||
});
|
||||
});
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function fetchBalance(state = {}, deps = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, '/v1/user/profile', {
|
||||
actionLabel: '5sim 查询余额',
|
||||
});
|
||||
return {
|
||||
balance: Number(payload?.balance),
|
||||
frozenBalance: Number(payload?.frozen_balance),
|
||||
rating: Number(payload?.rating),
|
||||
raw: payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchCountries(_state = {}, deps = {}) {
|
||||
const config = {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
const payload = await fetchJson(config, '/v1/guest/countries', {
|
||||
actionLabel: '5sim 查询国家列表',
|
||||
requireAuth: false,
|
||||
});
|
||||
return Object.entries(payload || {})
|
||||
.map(([slug, entry]) => {
|
||||
const id = normalizeFiveSimCountryId(slug, '');
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
label: formatFiveSimCountryLabel(id, entry?.text_en || slug, slug),
|
||||
searchText: [
|
||||
slug,
|
||||
formatFiveSimCountryLabel(id, entry?.text_en || slug, slug),
|
||||
entry?.text_en,
|
||||
entry?.text_ru,
|
||||
Object.keys(entry?.iso || {}).join(' '),
|
||||
Object.keys(entry?.prefix || {}).join(' '),
|
||||
].filter(Boolean).join(' '),
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => left.label.localeCompare(right.label));
|
||||
}
|
||||
|
||||
function collectPriceEntries(payload, entries = []) {
|
||||
if (Array.isArray(payload)) {
|
||||
payload.forEach((entry) => collectPriceEntries(entry, entries));
|
||||
return entries;
|
||||
}
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const cost = normalizePrice(payload.cost ?? payload.Price ?? payload.price);
|
||||
if (cost !== null) {
|
||||
const count = Number(payload.count ?? payload.Qty ?? payload.qty);
|
||||
entries.push({
|
||||
cost,
|
||||
count: Number.isFinite(count) ? count : 0,
|
||||
inStock: !Number.isFinite(count) || count > 0,
|
||||
});
|
||||
}
|
||||
|
||||
Object.values(payload).forEach((entry) => collectPriceEntries(entry, entries));
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function fetchPrices(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const config = {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
return fetchJson(config, '/v1/guest/prices', {
|
||||
query: {
|
||||
country: normalizeFiveSimCountryId(countryConfig?.id),
|
||||
product: DEFAULT_PRODUCT,
|
||||
},
|
||||
actionLabel: '5sim 查询价格',
|
||||
requireAuth: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchProducts(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const config = {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||
return fetchJson(
|
||||
config,
|
||||
`/v1/guest/products/${encodeURIComponent(normalizeFiveSimCountryId(countryConfig?.id))}/${encodeURIComponent(operator)}`,
|
||||
{
|
||||
actionLabel: '5sim 查询产品价格',
|
||||
requireAuth: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function resolvePricePlan(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const userLimitText = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||
const userLimit = userLimitText ? Number(userLimitText) : null;
|
||||
let priceCandidates = [];
|
||||
|
||||
try {
|
||||
const productsPayload = await fetchProducts(state, countryConfig, deps);
|
||||
const openaiProduct = productsPayload?.[DEFAULT_PRODUCT];
|
||||
const productPrice = normalizePrice(openaiProduct?.Price ?? openaiProduct?.price ?? openaiProduct?.cost);
|
||||
const productQty = Number(openaiProduct?.Qty ?? openaiProduct?.qty ?? openaiProduct?.count);
|
||||
if (productPrice !== null && (!Number.isFinite(productQty) || productQty > 0)) {
|
||||
priceCandidates.push(productPrice);
|
||||
}
|
||||
} catch (_) {
|
||||
// Products endpoint is only used to find the buy-compatible operator price.
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fetchPrices(state, countryConfig, deps);
|
||||
priceCandidates = [
|
||||
...priceCandidates,
|
||||
...buildSortedUniquePriceCandidates(
|
||||
collectPriceEntries(payload, [])
|
||||
.filter((entry) => entry.inStock)
|
||||
.map((entry) => entry.cost)
|
||||
),
|
||||
];
|
||||
} catch (_) {
|
||||
// Best-effort lookup. Purchase can still be attempted without a catalog price.
|
||||
}
|
||||
priceCandidates = buildSortedUniquePriceCandidates(priceCandidates);
|
||||
|
||||
const minCatalogPrice = priceCandidates.length > 0 ? priceCandidates[0] : null;
|
||||
if (userLimit !== null) {
|
||||
const bounded = priceCandidates.filter((price) => price <= userLimit);
|
||||
return {
|
||||
prices: bounded.length > 0 ? [userLimit, ...bounded.filter((price) => price !== userLimit)] : [userLimit],
|
||||
userLimit,
|
||||
minCatalogPrice,
|
||||
};
|
||||
}
|
||||
|
||||
if (priceCandidates.length > 0) {
|
||||
return { prices: priceCandidates, userLimit: null, minCatalogPrice };
|
||||
}
|
||||
return { prices: [null], userLimit: null, minCatalogPrice: null };
|
||||
}
|
||||
|
||||
function normalizeActivation(record, fallback = {}) {
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
||||
return null;
|
||||
}
|
||||
const activationId = String(record.activationId ?? record.id ?? '').trim();
|
||||
const phoneNumber = String(record.phoneNumber ?? record.phone ?? '').trim();
|
||||
if (!activationId || !phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
const countryId = getCountryIdFromPayload(record, fallback.countryId || DEFAULT_COUNTRY_ID);
|
||||
const countryLabel = getCountryLabelFromPayload(record, fallback.countryLabel || countryId, countryId);
|
||||
return {
|
||||
activationId,
|
||||
phoneNumber,
|
||||
provider: PROVIDER_ID,
|
||||
serviceCode: DEFAULT_PRODUCT,
|
||||
countryId,
|
||||
countryLabel,
|
||||
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
|
||||
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_MAX_USES)),
|
||||
operator: normalizeFiveSimOperator(record.operator || fallback.operator),
|
||||
...(record.price !== undefined ? { price: Number(record.price) } : {}),
|
||||
...(record.status ? { status: String(record.status) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function isNoNumbersPayload(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /no\s+free\s+phones|no\s+numbers|not\s+found/i.test(text);
|
||||
}
|
||||
|
||||
function isTerminalError(payloadOrMessage) {
|
||||
const text = describePayload(payloadOrMessage);
|
||||
return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text);
|
||||
}
|
||||
|
||||
async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||
const query = {};
|
||||
if (maxPrice !== null && maxPrice !== undefined) {
|
||||
query.maxPrice = maxPrice;
|
||||
}
|
||||
if (state.fiveSimReuseEnabled !== false) {
|
||||
query.reuse = 1;
|
||||
}
|
||||
const payload = await fetchJson(
|
||||
config,
|
||||
`/v1/user/buy/activation/${encodeURIComponent(normalizeFiveSimCountryId(countryConfig.id))}/${encodeURIComponent(operator)}/${DEFAULT_PRODUCT}`,
|
||||
{
|
||||
query,
|
||||
actionLabel: '5sim 购买手机号',
|
||||
}
|
||||
);
|
||||
return normalizeActivation(payload, {
|
||||
countryId: countryConfig.id,
|
||||
countryLabel: countryConfig.label,
|
||||
operator,
|
||||
});
|
||||
}
|
||||
|
||||
async function requestActivation(state = {}, options = {}, deps = {}) {
|
||||
const allCountryCandidates = resolveCountryCandidates(state);
|
||||
const blockedCountryIds = new Set(
|
||||
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
|
||||
.map((value) => normalizeFiveSimCountryId(value, ''))
|
||||
.filter(Boolean)
|
||||
);
|
||||
let countryCandidates = allCountryCandidates.filter((entry) => !blockedCountryIds.has(normalizeFiveSimCountryId(entry.id, '')));
|
||||
if (!countryCandidates.length) {
|
||||
countryCandidates = allCountryCandidates;
|
||||
if (blockedCountryIds.size && typeof deps.addLog === 'function') {
|
||||
await deps.addLog(
|
||||
'步骤 9:已选 5sim 国家均达到临时收码失败跳过阈值,本轮解除跳过并重新尝试。',
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const acquirePriority = String(state?.heroSmsAcquirePriority || 'country').trim().toLowerCase() === 'price' ? 'price' : 'country';
|
||||
const countryAttempts = countryCandidates.map((countryConfig, index) => ({
|
||||
index,
|
||||
countryConfig,
|
||||
pricePlan: null,
|
||||
orderingPrice: Number.POSITIVE_INFINITY,
|
||||
}));
|
||||
|
||||
if (acquirePriority === 'price') {
|
||||
for (const attempt of countryAttempts) {
|
||||
const pricePlan = await resolvePricePlan(state, attempt.countryConfig, deps);
|
||||
const numericPrices = Array.isArray(pricePlan?.prices)
|
||||
? pricePlan.prices.map(Number).filter((value) => Number.isFinite(value) && value >= 0)
|
||||
: [];
|
||||
const minCandidatePrice = numericPrices.length ? Math.min(...numericPrices) : null;
|
||||
const cappedByUserLimit = (
|
||||
pricePlan?.userLimit !== null
|
||||
&& pricePlan?.userLimit !== undefined
|
||||
&& pricePlan?.minCatalogPrice !== null
|
||||
&& pricePlan?.minCatalogPrice !== undefined
|
||||
&& Number(pricePlan.minCatalogPrice) > Number(pricePlan.userLimit)
|
||||
);
|
||||
attempt.pricePlan = pricePlan;
|
||||
attempt.orderingPrice = cappedByUserLimit
|
||||
? Number.POSITIVE_INFINITY
|
||||
: (minCandidatePrice !== null ? minCandidatePrice : Number.POSITIVE_INFINITY);
|
||||
}
|
||||
countryAttempts.sort((left, right) => (
|
||||
left.orderingPrice !== right.orderingPrice
|
||||
? left.orderingPrice - right.orderingPrice
|
||||
: left.index - right.index
|
||||
));
|
||||
}
|
||||
|
||||
const noNumbersByCountry = [];
|
||||
let lastError = null;
|
||||
let lastFailureText = '';
|
||||
for (const attempt of countryAttempts) {
|
||||
const countryConfig = attempt.countryConfig;
|
||||
const pricePlan = attempt.pricePlan || await resolvePricePlan(state, countryConfig, deps);
|
||||
for (const maxPrice of pricePlan.prices) {
|
||||
try {
|
||||
const activation = await buyActivationWithPrice(state, countryConfig, maxPrice, deps);
|
||||
if (activation) {
|
||||
return activation;
|
||||
}
|
||||
lastFailureText = '空响应';
|
||||
} catch (error) {
|
||||
const payloadOrMessage = error?.payload || error?.message;
|
||||
if (isTerminalError(payloadOrMessage)) {
|
||||
throw new Error(`5sim 购买手机号失败:${describePayload(payloadOrMessage) || '空响应'}`);
|
||||
}
|
||||
if (isNoNumbersPayload(payloadOrMessage)) {
|
||||
lastFailureText = describePayload(payloadOrMessage) || lastFailureText;
|
||||
continue;
|
||||
}
|
||||
lastError = error;
|
||||
lastFailureText = describePayload(payloadOrMessage) || lastFailureText;
|
||||
}
|
||||
}
|
||||
noNumbersByCountry.push(`${countryConfig.label}: ${lastFailureText || '无可用号码'}`);
|
||||
}
|
||||
|
||||
if (noNumbersByCountry.length) {
|
||||
throw new Error(`5sim 已尝试 ${countryCandidates.length} 个候选国家,均无可用号码:${noNumbersByCountry.join(' | ')}。`);
|
||||
}
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
throw new Error(`5sim 获取手机号失败,最后状态:${lastFailureText || '未知'}。`);
|
||||
}
|
||||
|
||||
async function reuseActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) {
|
||||
throw new Error('缺少可复用的 5sim 手机号订单。');
|
||||
}
|
||||
const phoneDigits = String(normalizedActivation.phoneNumber || '').replace(/\D+/g, '');
|
||||
if (!phoneDigits) {
|
||||
throw new Error('可复用的 5sim 手机号无效。');
|
||||
}
|
||||
const config = resolveConfig(state, deps);
|
||||
const numberWithoutPlus = String(normalizedActivation.phoneNumber || '')
|
||||
.replace(/^\+/, '')
|
||||
.replace(/[^0-9]+/g, '');
|
||||
const payload = await fetchJson(config, `/v1/user/reuse/${DEFAULT_PRODUCT}/${numberWithoutPlus || phoneDigits}`, {
|
||||
actionLabel: '5sim 复用手机号',
|
||||
});
|
||||
return normalizeActivation(payload, normalizedActivation);
|
||||
}
|
||||
|
||||
async function finishActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, `/v1/user/finish/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 完成订单',
|
||||
});
|
||||
return describePayload(payload);
|
||||
}
|
||||
|
||||
async function cancelActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, `/v1/user/cancel/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 取消订单',
|
||||
});
|
||||
return describePayload(payload);
|
||||
}
|
||||
|
||||
async function banActivation(state = {}, activation, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) return '';
|
||||
const config = resolveConfig(state, deps);
|
||||
const payload = await fetchJson(config, `/v1/user/ban/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 拉黑号码',
|
||||
});
|
||||
return describePayload(payload);
|
||||
}
|
||||
|
||||
function extractVerificationCode(rawCodeOrText) {
|
||||
const trimmed = String(rawCodeOrText || '').trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
const digitMatch = trimmed.match(/\b(\d{4,8})\b/);
|
||||
return digitMatch?.[1] || trimmed;
|
||||
}
|
||||
|
||||
function extractCodeFromOrder(payload) {
|
||||
const smsList = Array.isArray(payload?.sms) ? payload.sms : [];
|
||||
for (let index = smsList.length - 1; index >= 0; index -= 1) {
|
||||
const message = smsList[index] || {};
|
||||
const code = extractVerificationCode(message.code) || extractVerificationCode(message.text);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
||||
const normalizedActivation = normalizeActivation(activation);
|
||||
if (!normalizedActivation) {
|
||||
throw new Error('缺少手机号接码订单。');
|
||||
}
|
||||
const config = resolveConfig(state, deps);
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 180000);
|
||||
const intervalMs = Math.max(1000, Number(options.intervalMs) || 5000);
|
||||
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
|
||||
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
|
||||
const start = Date.now();
|
||||
let pollCount = 0;
|
||||
let lastResponse = '';
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (maxRounds > 0 && pollCount >= maxRounds) {
|
||||
break;
|
||||
}
|
||||
deps.throwIfStopped?.();
|
||||
const payload = await fetchJson(config, `/v1/user/check/${encodeURIComponent(normalizedActivation.activationId)}`, {
|
||||
actionLabel: '5sim 查询验证码',
|
||||
});
|
||||
pollCount += 1;
|
||||
lastResponse = describePayload(payload);
|
||||
if (typeof options.onStatus === 'function') {
|
||||
await options.onStatus({
|
||||
activation: normalizedActivation,
|
||||
elapsedMs: Date.now() - start,
|
||||
pollCount,
|
||||
statusText: String(payload?.status || lastResponse || '未知'),
|
||||
timeoutMs,
|
||||
});
|
||||
}
|
||||
const code = extractCodeFromOrder(payload);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
const status = String(payload?.status || '').trim().toUpperCase();
|
||||
if (['CANCELED', 'BANNED', 'FINISHED', 'TIMEOUT'].includes(status)) {
|
||||
throw new Error(`5sim 查询验证码失败:订单状态 ${status}`);
|
||||
}
|
||||
await deps.sleepWithStop(intervalMs);
|
||||
}
|
||||
|
||||
const suffix = lastResponse ? ` 5sim 最后状态:${lastResponse}` : '';
|
||||
throw new Error(`PHONE_CODE_TIMEOUT::等待手机验证码超时。${suffix}`);
|
||||
}
|
||||
|
||||
function createProvider(deps = {}) {
|
||||
const providerDeps = {
|
||||
fetchImpl: deps.fetchImpl,
|
||||
sleepWithStop: deps.sleepWithStop,
|
||||
throwIfStopped: deps.throwIfStopped,
|
||||
addLog: deps.addLog,
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
return {
|
||||
id: PROVIDER_ID,
|
||||
label: '5sim',
|
||||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||
defaultProduct: DEFAULT_PRODUCT,
|
||||
defaultOperator: DEFAULT_OPERATOR,
|
||||
normalizeCountryId: normalizeFiveSimCountryId,
|
||||
normalizeCountryLabel: normalizeFiveSimCountryLabel,
|
||||
formatCountryLabel: formatFiveSimCountryLabel,
|
||||
normalizeCountryFallback: normalizeFiveSimCountryFallback,
|
||||
normalizeMaxPrice: normalizeFiveSimMaxPrice,
|
||||
normalizeOperator: normalizeFiveSimOperator,
|
||||
resolveCountryCandidates,
|
||||
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
|
||||
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
|
||||
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
|
||||
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
|
||||
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
|
||||
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
|
||||
fetchBalance: (state) => fetchBalance(state, providerDeps),
|
||||
fetchCountries: (state) => fetchCountries(state, providerDeps),
|
||||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
||||
collectPriceEntries,
|
||||
describePayload,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
PROVIDER_ID,
|
||||
DEFAULT_COUNTRY_ID,
|
||||
DEFAULT_COUNTRY_LABEL,
|
||||
DEFAULT_OPERATOR,
|
||||
DEFAULT_PRODUCT,
|
||||
createProvider,
|
||||
normalizeFiveSimCountryFallback,
|
||||
normalizeFiveSimCountryId,
|
||||
normalizeFiveSimCountryLabel,
|
||||
formatFiveSimCountryLabel,
|
||||
normalizeFiveSimMaxPrice,
|
||||
normalizeFiveSimOperator,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
// phone-sms/providers/hero-sms.js — HeroSMS 接码平台适配层
|
||||
(function attachHeroSmsProvider(root, factory) {
|
||||
root.PhoneSmsHeroSmsProvider = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createHeroSmsProviderModule() {
|
||||
const PROVIDER_ID = 'hero-sms';
|
||||
const DEFAULT_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php';
|
||||
const DEFAULT_SERVICE_CODE = 'dr';
|
||||
const DEFAULT_SERVICE_LABEL = 'OpenAI';
|
||||
const DEFAULT_COUNTRY_ID = 52;
|
||||
const DEFAULT_COUNTRY_LABEL = 'Thailand';
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||||
|
||||
function normalizeHeroSmsCountryId(value, fallback = DEFAULT_COUNTRY_ID) {
|
||||
const parsed = Math.floor(Number(value));
|
||||
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
||||
const fallbackParsed = Math.floor(Number(fallback));
|
||||
return Number.isFinite(fallbackParsed) && fallbackParsed > 0 ? fallbackParsed : DEFAULT_COUNTRY_ID;
|
||||
}
|
||||
|
||||
function normalizeHeroSmsCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||||
return String(value || '').trim() || fallback;
|
||||
}
|
||||
|
||||
function normalizeHeroSmsMaxPrice(value = '') {
|
||||
const rawValue = String(value ?? '').trim();
|
||||
if (!rawValue) return '';
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) return '';
|
||||
return String(Math.round(numeric * 10000) / 10000);
|
||||
}
|
||||
|
||||
function normalizeHeroSmsCountryFallback(value = []) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(/[\r\n,,;;]+/)
|
||||
.map((entry) => String(entry || '').trim())
|
||||
.filter(Boolean);
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
for (const entry of source) {
|
||||
let id = 0;
|
||||
let label = '';
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
id = normalizeHeroSmsCountryId(entry.id ?? entry.countryId, 0);
|
||||
label = String((entry.label ?? entry.countryLabel) || '').trim();
|
||||
} else {
|
||||
const text = String(entry || '').trim();
|
||||
const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||||
id = normalizeHeroSmsCountryId(structured?.[1] || text, 0);
|
||||
label = String(structured?.[2] || '').trim();
|
||||
}
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
normalized.push({ id, label: label || `Country #${id}` });
|
||||
if (normalized.length >= 20) break;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value = '') {
|
||||
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
|
||||
try {
|
||||
return new URL(trimmed).toString();
|
||||
} catch {
|
||||
return DEFAULT_BASE_URL;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(config = {}, query = {}) {
|
||||
const url = new URL(normalizeBaseUrl(config.baseUrl));
|
||||
Object.entries(query || {}).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
url.searchParams.set(key, String(value));
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function parsePayload(text) {
|
||||
const trimmed = String(text || '').trim();
|
||||
if (!trimmed) return '';
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
try { return JSON.parse(trimmed); } catch { return trimmed; }
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function describePayload(raw) {
|
||||
if (typeof raw === 'string') return raw.trim();
|
||||
if (raw && typeof raw === 'object') {
|
||||
const direct = String(raw.message || raw.msg || raw.error || raw.title || raw.status || '').trim();
|
||||
if (direct) return direct;
|
||||
try { return JSON.stringify(raw); } catch { return String(raw); }
|
||||
}
|
||||
return String(raw || '').trim();
|
||||
}
|
||||
|
||||
function resolveConfig(state = {}, deps = {}) {
|
||||
return {
|
||||
apiKey: String(state.heroSmsApiKey || '').trim(),
|
||||
baseUrl: state.heroSmsBaseUrl || DEFAULT_BASE_URL,
|
||||
fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null),
|
||||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchPayload(config, query, actionLabel = 'HeroSMS request') {
|
||||
if (query.api_key === undefined && config.apiKey) {
|
||||
query = { api_key: config.apiKey, ...query };
|
||||
}
|
||||
if (!config.fetchImpl) {
|
||||
throw new Error('HeroSMS fetch implementation is unavailable.');
|
||||
}
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||
const timeoutId = controller
|
||||
? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS)
|
||||
: null;
|
||||
try {
|
||||
const response = await config.fetchImpl(buildUrl(config, query), {
|
||||
method: 'GET',
|
||||
signal: controller?.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
const payload = parsePayload(text);
|
||||
if (!response.ok) {
|
||||
const error = new Error(`${actionLabel} failed: ${describePayload(payload) || response.status}`);
|
||||
error.payload = payload;
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`${actionLabel} timed out.`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCountryConfig(state = {}) {
|
||||
return {
|
||||
id: normalizeHeroSmsCountryId(state.heroSmsCountryId),
|
||||
label: normalizeHeroSmsCountryLabel(state.heroSmsCountryLabel),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCountryCandidates(state = {}) {
|
||||
const primary = resolveCountryConfig(state);
|
||||
const seen = new Set([primary.id]);
|
||||
const candidates = [primary];
|
||||
normalizeHeroSmsCountryFallback(state.heroSmsCountryFallback).forEach((entry) => {
|
||||
const id = normalizeHeroSmsCountryId(entry.id, 0);
|
||||
if (!id || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
candidates.push({ id, label: normalizeHeroSmsCountryLabel(entry.label, `Country #${id}`) });
|
||||
});
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function fetchBalance(state = {}, deps = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
if (!config.apiKey) {
|
||||
throw new Error('HeroSMS API key is missing. Save it in the side panel before querying balance.');
|
||||
}
|
||||
const payload = await fetchPayload(config, { action: 'getBalance' }, 'HeroSMS getBalance');
|
||||
const balance = Number(String(describePayload(payload)).replace(/^ACCESS_BALANCE:/i, '').trim());
|
||||
return { balance, raw: payload };
|
||||
}
|
||||
|
||||
async function fetchPrices(state = {}, countryConfig = resolveCountryConfig(state), deps = {}) {
|
||||
const config = resolveConfig(state, deps);
|
||||
return fetchPayload(config, {
|
||||
action: 'getPrices',
|
||||
service: DEFAULT_SERVICE_CODE,
|
||||
country: normalizeHeroSmsCountryId(countryConfig?.id),
|
||||
}, 'HeroSMS getPrices');
|
||||
}
|
||||
|
||||
function createProvider(deps = {}) {
|
||||
return {
|
||||
id: PROVIDER_ID,
|
||||
label: 'HeroSMS',
|
||||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||||
defaultProduct: DEFAULT_SERVICE_LABEL,
|
||||
normalizeCountryId: normalizeHeroSmsCountryId,
|
||||
normalizeCountryLabel: normalizeHeroSmsCountryLabel,
|
||||
normalizeCountryFallback: normalizeHeroSmsCountryFallback,
|
||||
normalizeMaxPrice: normalizeHeroSmsMaxPrice,
|
||||
resolveCountryCandidates,
|
||||
fetchBalance: (state) => fetchBalance(state, deps),
|
||||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, deps),
|
||||
describePayload,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
PROVIDER_ID,
|
||||
DEFAULT_BASE_URL,
|
||||
DEFAULT_COUNTRY_ID,
|
||||
DEFAULT_COUNTRY_LABEL,
|
||||
DEFAULT_SERVICE_CODE,
|
||||
DEFAULT_SERVICE_LABEL,
|
||||
createProvider,
|
||||
describePayload,
|
||||
normalizeHeroSmsCountryFallback,
|
||||
normalizeHeroSmsCountryId,
|
||||
normalizeHeroSmsCountryLabel,
|
||||
normalizeHeroSmsMaxPrice,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
// phone-sms/providers/registry.js — 接码平台注册表
|
||||
(function attachPhoneSmsProviderRegistry(root, factory) {
|
||||
root.PhoneSmsProviderRegistry = factory(root);
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createPhoneSmsProviderRegistry(root) {
|
||||
const PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PROVIDER = PROVIDER_HERO_SMS;
|
||||
|
||||
function normalizeProviderId(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === PROVIDER_FIVE_SIM ? PROVIDER_FIVE_SIM : PROVIDER_HERO_SMS;
|
||||
}
|
||||
|
||||
function getProviderModule(providerId = DEFAULT_PROVIDER) {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
if (normalized === PROVIDER_FIVE_SIM) {
|
||||
return root.PhoneSmsFiveSimProvider || null;
|
||||
}
|
||||
return root.PhoneSmsHeroSmsProvider || null;
|
||||
}
|
||||
|
||||
function createProvider(providerId = DEFAULT_PROVIDER, deps = {}) {
|
||||
const module = getProviderModule(providerId);
|
||||
if (!module || typeof module.createProvider !== 'function') {
|
||||
throw new Error(`Phone SMS provider is not loaded: ${normalizeProviderId(providerId)}`);
|
||||
}
|
||||
return module.createProvider(deps);
|
||||
}
|
||||
|
||||
function getProviderLabel(providerId = DEFAULT_PROVIDER) {
|
||||
return normalizeProviderId(providerId) === PROVIDER_FIVE_SIM ? '5sim' : 'HeroSMS';
|
||||
}
|
||||
|
||||
return {
|
||||
PROVIDER_HERO_SMS,
|
||||
PROVIDER_FIVE_SIM,
|
||||
DEFAULT_PROVIDER,
|
||||
normalizeProviderId,
|
||||
getProviderModule,
|
||||
createProvider,
|
||||
getProviderLabel,
|
||||
};
|
||||
});
|
||||
@@ -466,6 +466,343 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="phone-verification-section" class="data-card phone-verification-card">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
<span class="section-label">接码设置</span>
|
||||
<span class="data-value">手机号验证与接码平台获取策略</span>
|
||||
</div>
|
||||
<div id="row-phone-verification-enabled" class="section-mini-actions phone-verification-header-actions">
|
||||
<button id="btn-toggle-phone-verification-section" class="btn btn-ghost btn-xs" type="button"
|
||||
aria-expanded="false" aria-controls="row-phone-verification-fold">展开设置</button>
|
||||
<label class="toggle-switch" for="input-phone-verification-enabled" title="启用或禁用手机号接码流程">
|
||||
<input type="checkbox" id="input-phone-verification-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-platform">
|
||||
<span class="data-label">接码平台</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<select id="select-phone-sms-provider" class="data-input mono" title="选择步骤 9 使用的接码平台">
|
||||
<option value="hero-sms">HeroSMS</option>
|
||||
<option value="5sim">5sim</option>
|
||||
</select>
|
||||
<span id="display-hero-sms-platform" class="data-value mono data-value-fill">HeroSMS / OpenAI / Thailand</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row phone-verification-fold-row" id="row-phone-verification-fold" style="display:none;">
|
||||
<div id="phone-verification-fold" class="phone-verification-fold">
|
||||
<div class="phone-verification-fold-body">
|
||||
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
|
||||
<span class="data-label">同步服务</span>
|
||||
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono"
|
||||
placeholder="http://127.0.0.1:17373" />
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-country" style="display:none;">
|
||||
<span class="data-label">国家优先级</span>
|
||||
<div class="data-inline hero-sms-country-stack">
|
||||
<select id="select-hero-sms-country" class="data-input mono" multiple size="6" style="display:none;">
|
||||
<option value="52" selected>Thailand</option>
|
||||
</select>
|
||||
<div class="hero-sms-country-mainline">
|
||||
<div id="hero-sms-country-menu-shell" class="hero-sms-country-menu">
|
||||
<button id="btn-hero-sms-country-menu" class="btn btn-outline btn-sm hero-sms-country-menu-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||
Thailand (1/3)
|
||||
</button>
|
||||
<div id="hero-sms-country-menu" class="hero-sms-country-menu-dropdown" role="listbox" aria-multiselectable="true" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="data-value hero-sms-country-note">多选最多 3 个,按点击顺序生效。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-country-fallback" style="display:none;">
|
||||
<span class="data-label">生效顺序</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-hero-sms-country-fallback-order" class="data-value data-value-fill mono">Thailand(52)</span>
|
||||
<button id="btn-hero-sms-country-clear" class="btn btn-ghost btn-xs data-inline-btn" type="button">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-acquire-priority" style="display:none;">
|
||||
<span class="data-label">拿号优先级</span>
|
||||
<select id="select-hero-sms-acquire-priority" class="data-input mono">
|
||||
<option value="country">国家优先(默认)</option>
|
||||
<option value="price">价格优先(同价按国家顺序)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
||||
<span class="data-label">接码 API</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-hero-sms-api-key" class="data-input data-input-with-icon mono"
|
||||
placeholder="请输入 HeroSMS API Key" />
|
||||
<button id="btn-toggle-hero-sms-api-key" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-hero-sms-api-key" data-show-label="显示 HeroSMS API Key"
|
||||
data-hide-label="隐藏 HeroSMS API Key" aria-label="显示 HeroSMS API Key" title="显示 HeroSMS API Key"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
|
||||
<span class="data-label">价格</span>
|
||||
<div class="data-inline hero-sms-price-preview-stack">
|
||||
<div class="hero-sms-price-preview-head">
|
||||
<button id="btn-hero-sms-price-preview" class="btn btn-outline btn-xs data-inline-btn" type="button">查询价格</button>
|
||||
<button id="btn-phone-sms-balance" class="btn btn-ghost btn-xs data-inline-btn" type="button">查余额</button>
|
||||
</div>
|
||||
<div id="row-hero-sms-price-tiers" class="hero-sms-price-preview-result" style="display:none;">
|
||||
<span id="display-hero-sms-price-tiers" class="data-value mono hero-sms-price-preview-text">未获取</span>
|
||||
<span id="display-phone-sms-balance" class="data-value mono hero-sms-price-preview-text">余额未获取</span>
|
||||
</div>
|
||||
<div class="hero-sms-price-controls-grid">
|
||||
<div class="hero-sms-price-control">
|
||||
<span class="hero-sms-settings-caption">价格上限</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-hero-sms-max-price" class="data-input auto-delay-input mono hero-sms-max-price-input" placeholder="0.12" min="0" step="0.0001" title="接码价格上限;可空(空=自动价格)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-sms-price-control hero-sms-price-control-reuse">
|
||||
<span class="hero-sms-settings-caption">号码复用</span>
|
||||
<div class="setting-controls hero-sms-toggle-controls">
|
||||
<label class="toggle-switch hero-sms-price-reuse-toggle" for="input-hero-sms-reuse-enabled" title="开启后会优先复用未超次数的可用号码">
|
||||
<input type="checkbox" id="input-hero-sms-reuse-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-five-sim-operator" style="display:none;">
|
||||
<span class="data-label">5sim Operator</span>
|
||||
<input type="text" id="input-five-sim-operator" class="data-input mono" placeholder="any" title="5sim operator,默认 any" />
|
||||
</div>
|
||||
<div class="data-row" id="row-phone-code-settings-group" style="display:none;">
|
||||
<span class="data-label">接码参数</span>
|
||||
<div class="data-inline hero-sms-settings-grid">
|
||||
<div id="row-phone-verification-resend-count" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">验证码重发</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
|
||||
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-replacement-limit" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">换号上限</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-replacement-limit" class="data-input auto-delay-input" value="3" min="1" max="20" step="1" title="步骤 9 内部允许更换号码的最大次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-wait-seconds" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">验证码限时</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-wait-seconds" class="data-input auto-delay-input" value="60" min="15" max="300" step="1" title="每轮等待验证码的秒数" />
|
||||
<span class="data-unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-timeout-windows" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">超时次数</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-timeout-windows" class="data-input auto-delay-input" value="2" min="1" max="10" step="1" title="验证码超时后,最多继续等待几轮再换号" />
|
||||
<span class="data-unit">轮</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-poll-interval-seconds" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">轮询间隔</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-poll-interval-seconds" class="data-input auto-delay-input" value="5" min="1" max="30" step="1" title="向 HeroSMS 查询验证码状态的间隔秒数" />
|
||||
<span class="data-unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="row-phone-code-poll-max-rounds" class="hero-sms-settings-cell" style="display:none;">
|
||||
<span class="hero-sms-settings-caption">轮询次数</span>
|
||||
<div class="setting-controls">
|
||||
<input type="number" id="input-phone-code-poll-max-rounds" class="data-input auto-delay-input" value="4" min="1" max="120" step="1" title="每轮验证码等待窗口最多轮询次数" />
|
||||
<span class="data-unit">次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hero-sms-runtime-pair" style="display:none;">
|
||||
<span class="data-label">运行状态</span>
|
||||
<div class="data-inline hero-sms-runtime-grid">
|
||||
<div id="row-hero-sms-current-number" class="hero-sms-runtime-cell" style="display:none;">
|
||||
<span class="hero-sms-runtime-key">当前分配</span>
|
||||
<span id="display-hero-sms-current-number" class="data-value mono hero-sms-runtime-value">未分配</span>
|
||||
</div>
|
||||
<div id="row-hero-sms-current-code" class="hero-sms-runtime-cell" style="display:none;">
|
||||
<span class="hero-sms-runtime-key">验证码</span>
|
||||
<span id="display-hero-sms-current-code" class="data-value mono hero-sms-runtime-value">未获取</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ip-proxy-section" class="data-card ip-proxy-card">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
<span class="section-label">IP 代理</span>
|
||||
<span class="data-value">用于浏览器代理接管与出口切换</span>
|
||||
</div>
|
||||
<div id="row-ip-proxy-enabled" class="section-mini-actions ip-proxy-header-actions">
|
||||
<button id="btn-toggle-ip-proxy-section" class="btn btn-ghost btn-xs" type="button"
|
||||
aria-expanded="false" aria-controls="row-ip-proxy-fold">展开设置</button>
|
||||
<label class="toggle-switch" for="input-ip-proxy-enabled" title="启用或禁用 IP 代理接管">
|
||||
<input type="checkbox" id="input-ip-proxy-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-fold-row" id="row-ip-proxy-fold" style="display:none;">
|
||||
<div id="ip-proxy-fold" class="ip-proxy-fold">
|
||||
<div class="ip-proxy-fold-body">
|
||||
<div class="data-row" id="row-ip-proxy-service" style="display:none;">
|
||||
<span class="data-label">代理服务</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-ip-proxy-service" class="data-select" disabled>
|
||||
<option value="711proxy">711Proxy(首版)</option>
|
||||
</select>
|
||||
<button id="btn-ip-proxy-service-login" class="btn btn-outline btn-sm data-inline-btn" type="button">
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-ip-proxy-mode" style="display:none;">
|
||||
<span class="data-label">代理模式</span>
|
||||
<div id="ip-proxy-mode-group" class="choice-group" role="group" aria-label="IP代理模式">
|
||||
<button type="button" class="choice-btn" data-ip-proxy-mode="account">账号密码</button>
|
||||
<button type="button" class="choice-btn" data-ip-proxy-mode="api" disabled title="首版暂未开放">API 拉取(暂未开放)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-layout-row" id="row-ip-proxy-layout" style="display:none;">
|
||||
<div class="ip-proxy-layout" id="ip-proxy-layout">
|
||||
<div class="ip-proxy-column ip-proxy-column-account" id="ip-proxy-account-panel">
|
||||
<div class="ip-proxy-column-header">账号密码模式</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-list" style="display:none;">
|
||||
<span class="data-label">账号代理列表</span>
|
||||
<textarea id="input-ip-proxy-account-list" class="data-textarea mono"
|
||||
placeholder="每行一条:host:port 或 host:port:username:password 例如 global.rotgb.711proxy.com:10000:username:password"></textarea>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-session-prefix" style="display:none;">
|
||||
<span class="data-label">会话(session)</span>
|
||||
<input type="text" id="input-ip-proxy-account-session-prefix" class="data-input mono"
|
||||
placeholder="会话前缀,例如 ZC28qZ0KQL" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-account-life-minutes" style="display:none;">
|
||||
<span class="data-label">时长(life)</span>
|
||||
<div class="data-inline">
|
||||
<input type="number" id="input-ip-proxy-account-life-minutes" class="data-input" min="1" max="1440" step="1"
|
||||
placeholder="5-180(分钟)" title="711Proxy 会话时长范围:5-180 分钟" />
|
||||
<span class="data-unit">分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-host" style="display:none;">
|
||||
<span class="data-label">代理 Host</span>
|
||||
<input type="text" id="input-ip-proxy-host" class="data-input" placeholder="例如 global.rotgb.711proxy.com" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-port" style="display:none;">
|
||||
<span class="data-label">代理 Port</span>
|
||||
<input type="number" id="input-ip-proxy-port" class="data-input" min="1" max="65535" step="1"
|
||||
placeholder="例如 10000" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-protocol" style="display:none;">
|
||||
<span class="data-label">代理协议</span>
|
||||
<select id="select-ip-proxy-protocol" class="data-select">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
<option value="socks4">SOCKS4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-username" style="display:none;">
|
||||
<span class="data-label">代理账号</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-ip-proxy-username" class="data-input data-input-with-icon"
|
||||
placeholder="例如 USER047152-zone-custom-region-US-asn-ASN81" />
|
||||
<button id="btn-toggle-ip-proxy-username" class="input-icon-btn" type="button" aria-label="显示代理账号"
|
||||
title="显示代理账号"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-password" style="display:none;">
|
||||
<span class="data-label">代理密码</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-ip-proxy-password" class="data-input data-input-with-icon"
|
||||
placeholder="账号密码代理的密码" />
|
||||
<button id="btn-toggle-ip-proxy-password" class="input-icon-btn" type="button" aria-label="显示代理密码"
|
||||
title="显示代理密码"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-region" style="display:none;">
|
||||
<span class="data-label">地区参数</span>
|
||||
<input type="text" id="input-ip-proxy-region" class="data-input" placeholder="可选,例如 US / DE / HK" />
|
||||
</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-pool-target-count" style="display:none;">
|
||||
<span class="data-label">任务切换阈值</span>
|
||||
<div class="data-inline">
|
||||
<input type="number" id="input-ip-proxy-pool-target-count" class="data-input" min="1" max="500" step="1"
|
||||
placeholder="20(成功轮次)" title="每成功多少轮任务后自动切换一次代理;仅 1 条节点时会跳过自动切换" />
|
||||
<span class="data-unit">轮</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ip-proxy-column ip-proxy-column-api" id="ip-proxy-api-panel">
|
||||
<div class="ip-proxy-column-header">API 模式</div>
|
||||
<div class="data-row ip-proxy-column-row" id="row-ip-proxy-api-url" style="display:none;">
|
||||
<span class="data-label">代理API</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-ip-proxy-api-url" class="data-input data-input-with-icon"
|
||||
placeholder="粘贴完整 API 链接" />
|
||||
<button id="btn-toggle-ip-proxy-api-url" class="input-icon-btn" type="button" aria-label="显示代理 API"
|
||||
title="显示代理 API"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-ip-proxy-actions" style="display:none;">
|
||||
<span class="data-label">切换代理</span>
|
||||
<div class="data-inline ip-proxy-actions-inline">
|
||||
<span id="ip-proxy-action-buttons" class="ip-proxy-action-grid">
|
||||
<button id="btn-ip-proxy-refresh" class="btn btn-outline btn-sm data-inline-btn" type="button">拉取</button>
|
||||
<button id="btn-ip-proxy-next" class="btn btn-outline btn-sm data-inline-btn" type="button">下一条</button>
|
||||
<button id="btn-ip-proxy-change" class="btn btn-outline btn-sm data-inline-btn" type="button" title="保持当前会话并刷新出口(仅 711 + session)">Change</button>
|
||||
<button id="btn-ip-proxy-probe" class="btn btn-outline btn-sm data-inline-btn" type="button">检测出口</button>
|
||||
</span>
|
||||
<span id="ip-proxy-action-hint" class="ip-proxy-action-hint">
|
||||
下一条:切到代理池下一条。Change:保持当前 session 重绑链路(仅 711 + session)。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-ip-proxy-runtime-status" style="display:none;">
|
||||
<span class="data-label">代理状态</span>
|
||||
<div id="ip-proxy-runtime-status" class="ip-proxy-runtime-status state-idle">
|
||||
<span id="ip-proxy-runtime-dot" class="ip-proxy-runtime-dot" aria-hidden="true"></span>
|
||||
<div class="ip-proxy-runtime-content">
|
||||
<div id="ip-proxy-runtime-text" class="ip-proxy-runtime-main">未启用,沿用浏览器默认/全局代理。</div>
|
||||
<div class="ip-proxy-runtime-meta">
|
||||
<span id="ip-proxy-current" class="ip-proxy-runtime-current">暂无可用代理</span>
|
||||
</div>
|
||||
<div class="ip-proxy-runtime-details-row">
|
||||
<details id="ip-proxy-runtime-details" class="ip-proxy-runtime-details" hidden>
|
||||
<summary>详情</summary>
|
||||
<div id="ip-proxy-runtime-details-text" class="ip-proxy-runtime-details-text"></div>
|
||||
</details>
|
||||
<button id="btn-ip-proxy-check-ip" class="btn btn-outline btn-xs ip-proxy-check-ip-btn" type="button">检查IP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
|
||||
<div class="section-mini-header">
|
||||
<div class="section-mini-copy">
|
||||
@@ -1446,6 +1783,9 @@
|
||||
<script src="../managed-alias-utils.js"></script>
|
||||
<script src="../mail2925-utils.js"></script>
|
||||
<script src="../paypal-utils.js"></script>
|
||||
<script src="../phone-sms/providers/hero-sms.js"></script>
|
||||
<script src="../phone-sms/providers/five-sim.js"></script>
|
||||
<script src="../phone-sms/providers/registry.js"></script>
|
||||
<script src="../icloud-utils.js"></script>
|
||||
<script src="../mail-provider-utils.js"></script>
|
||||
<script src="../hotmail-utils.js"></script>
|
||||
|
||||
+784
-1216
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,12 @@ test('background account history settings are normalized independently from hotm
|
||||
extractFunction('normalizePhoneCodePollMaxRounds'),
|
||||
extractFunction('normalizeHeroSmsMaxPrice'),
|
||||
extractFunction('normalizeHeroSmsCountryFallback'),
|
||||
extractFunction('normalizePhoneSmsProvider'),
|
||||
extractFunction('normalizeFiveSimCountryId'),
|
||||
extractFunction('normalizeFiveSimCountryLabel'),
|
||||
extractFunction('normalizeFiveSimOperator'),
|
||||
extractFunction('normalizeFiveSimMaxPrice'),
|
||||
extractFunction('normalizeFiveSimCountryFallback'),
|
||||
extractFunction('normalizePersistentSettingValue'),
|
||||
].join('\n');
|
||||
|
||||
@@ -98,6 +104,11 @@ const VERIFICATION_RESEND_COUNT_MIN = 0;
|
||||
const VERIFICATION_RESEND_COUNT_MAX = 20;
|
||||
const HERO_SMS_COUNTRY_ID = 52;
|
||||
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const FIVE_SIM_OPERATOR = 'any';
|
||||
const PERSISTED_SETTING_DEFAULTS = {
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
@@ -142,7 +153,19 @@ return {
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneCodePollMaxRounds', '18'), 18);
|
||||
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
|
||||
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
|
||||
assert.equal(api.normalizePersistentSettingValue('heroSmsCountryId', 0), 0);
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'england');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'england');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '英国 (England)');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '9.87654'), '9.8765');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '-1'), '');
|
||||
assert.equal(api.normalizePersistentSettingValue('fiveSimOperator', ''), 'any');
|
||||
assert.deepStrictEqual(
|
||||
api.normalizePersistentSettingValue('fiveSimCountryFallback', [{ id: 'usa', label: 'USA' }, 'thailand:Thailand']),
|
||||
[{ id: 'usa', label: 'USA' }, { id: 'thailand', label: 'Thailand' }]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
|
||||
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.PhoneSmsFiveSimProvider;`)({});
|
||||
|
||||
function createTextResponse(payload, ok = true, status = ok ? 200 : 400) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
text: async () => (typeof payload === 'string' ? payload : JSON.stringify(payload)),
|
||||
};
|
||||
}
|
||||
|
||||
test('5sim provider fetches profile balance with bearer token', async () => {
|
||||
const requests = [];
|
||||
const provider = api.createProvider({
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
requests.push({ url: new URL(url), options });
|
||||
return createTextResponse({ balance: 123.45, frozen_balance: 6.7, rating: 99 });
|
||||
},
|
||||
});
|
||||
|
||||
const balance = await provider.fetchBalance({ fiveSimApiKey: 'demo-key' });
|
||||
|
||||
assert.equal(requests[0].url.pathname, '/v1/user/profile');
|
||||
assert.equal(requests[0].options.headers.Authorization, 'Bearer demo-key');
|
||||
assert.equal(balance.balance, 123.45);
|
||||
assert.equal(balance.frozenBalance, 6.7);
|
||||
});
|
||||
|
||||
test('5sim provider maps countries and prices', async () => {
|
||||
const requests = [];
|
||||
const provider = api.createProvider({
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
requests.push({ url: parsed, options });
|
||||
if (parsed.pathname === '/v1/guest/countries') {
|
||||
return createTextResponse({ england: { text_en: 'England', iso: { GB: 1 }, prefix: { 44: 1 } } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return createTextResponse({ england: { any: { openai: { cost: 10, count: 2 } } } });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
});
|
||||
|
||||
const countries = await provider.fetchCountries({});
|
||||
const prices = await provider.fetchPrices({}, { id: 'england', label: 'England' });
|
||||
const entries = provider.collectPriceEntries(prices, []);
|
||||
|
||||
assert.deepStrictEqual(countries[0], {
|
||||
id: 'england',
|
||||
label: '英国 (England)',
|
||||
searchText: 'england 英国 (England) England GB 44',
|
||||
});
|
||||
assert.equal(requests[1].url.searchParams.get('country'), 'england');
|
||||
assert.equal(requests[1].url.searchParams.get('product'), 'openai');
|
||||
assert.deepStrictEqual(entries, [{ cost: 10, count: 2, inStock: true }]);
|
||||
});
|
||||
|
||||
test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation', async () => {
|
||||
const requests = [];
|
||||
const provider = api.createProvider({
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
requests.push({ url: parsed, options });
|
||||
if (parsed.pathname === '/v1/guest/products/england/any') {
|
||||
return createTextResponse({ openai: { Category: 'activation', Qty: 4, Price: 8 } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return createTextResponse({ england: { any: { openai: { cost: 9.5, count: 4 } } } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/buy/activation/england/any/openai') {
|
||||
return createTextResponse({ id: 1001, phone: '+447911123456', country: 'england', operator: 'any', status: 'PENDING' });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/check/1001') {
|
||||
return createTextResponse({ id: 1001, phone: '+447911123456', status: 'RECEIVED', sms: [{ text: 'code 112233' }] });
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/finish/1001') return createTextResponse({ status: 'FINISHED' });
|
||||
if (parsed.pathname === '/v1/user/cancel/1001') return createTextResponse({ status: 'CANCELED' });
|
||||
if (parsed.pathname === '/v1/user/ban/1001') return createTextResponse({ status: 'BANNED' });
|
||||
if (parsed.pathname === '/v1/user/reuse/openai/447911123456') {
|
||||
return createTextResponse({ id: 1002, phone: '+447911123456', country: 'england', status: 'PENDING' });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const state = { fiveSimApiKey: 'demo-key', fiveSimCountryId: 'england', fiveSimCountryLabel: 'England', fiveSimMaxPrice: '12', fiveSimOperator: 'any' };
|
||||
const activation = await provider.requestActivation(state);
|
||||
const code = await provider.pollActivationCode(state, activation, { timeoutMs: 1000, intervalMs: 1, maxRounds: 1 });
|
||||
await provider.finishActivation(state, activation);
|
||||
await provider.cancelActivation(state, activation);
|
||||
await provider.banActivation(state, activation);
|
||||
const reused = await provider.reuseActivation(state, activation);
|
||||
|
||||
assert.equal(activation.provider, '5sim');
|
||||
assert.equal(activation.activationId, '1001');
|
||||
assert.equal(activation.countryId, 'england');
|
||||
assert.equal(code, '112233');
|
||||
assert.equal(reused.activationId, '1002');
|
||||
const buy = requests.find((entry) => entry.url.pathname.includes('/buy/activation'));
|
||||
assert.equal(buy.url.searchParams.get('maxPrice'), '12');
|
||||
assert.equal(buy.url.searchParams.get('reuse'), '1');
|
||||
assert.deepStrictEqual(
|
||||
requests.map((entry) => entry.url.pathname),
|
||||
[
|
||||
'/v1/guest/products/england/any',
|
||||
'/v1/guest/prices',
|
||||
'/v1/user/buy/activation/england/any/openai',
|
||||
'/v1/user/check/1001',
|
||||
'/v1/user/finish/1001',
|
||||
'/v1/user/cancel/1001',
|
||||
'/v1/user/ban/1001',
|
||||
'/v1/user/reuse/openai/447911123456',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('5sim provider prefers buy-compatible products price over operator detail price', async () => {
|
||||
const requests = [];
|
||||
const provider = api.createProvider({
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
requests.push({ url: parsed, options });
|
||||
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
||||
return createTextResponse({ openai: { Category: 'activation', Qty: 4609, Price: 0.08 } });
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return createTextResponse({
|
||||
vietnam: {
|
||||
openai: {
|
||||
virtual21: { cost: 0.0769, count: 0 },
|
||||
virtual47: { cost: 0.1282, count: 4608 },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
|
||||
return createTextResponse({ id: 2001, phone: '+84901234567', country: 'vietnam', operator: 'any', status: 'PENDING' });
|
||||
}
|
||||
throw new Error(`unexpected ${parsed.pathname}`);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const activation = await provider.requestActivation({
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'vietnam',
|
||||
fiveSimCountryLabel: '越南 (Vietnam)',
|
||||
fiveSimOperator: 'any',
|
||||
});
|
||||
|
||||
assert.equal(activation.activationId, '2001');
|
||||
const buy = requests.find((entry) => entry.url.pathname.includes('/buy/activation'));
|
||||
assert.equal(buy.url.searchParams.get('maxPrice'), '0.08');
|
||||
assert.deepStrictEqual(
|
||||
requests.map((entry) => entry.url.pathname),
|
||||
[
|
||||
'/v1/guest/products/vietnam/any',
|
||||
'/v1/guest/prices',
|
||||
'/v1/user/buy/activation/vietnam/any/openai',
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -440,11 +440,11 @@ test('phone verification helper retries acquisition rounds when at least one cou
|
||||
assert.equal(sleeps.length, 1);
|
||||
assert.equal(sleeps[0], 2000);
|
||||
assert.equal(
|
||||
logs.filter((entry) => String(entry.message || '').includes('HeroSMS acquiring phone number')).length >= 2,
|
||||
logs.filter((entry) => String(entry.message || '').includes('HeroSMS 正在获取手机号')).length >= 2,
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
logs.some((entry) => String(entry.message || '').includes('HeroSMS has no available numbers (round 1/2); retrying')),
|
||||
logs.some((entry) => String(entry.message || '').includes('HeroSMS 暂无可用号码(第 1/2 轮)')),
|
||||
true
|
||||
);
|
||||
});
|
||||
@@ -1864,7 +1864,7 @@ test('phone verification helper replaces numbers in step 9 and stops after repla
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/did not succeed after 3 number replacements/i
|
||||
/更换 3 次号码后手机号验证仍未成功/
|
||||
);
|
||||
assert.ok(statusCallsById['123456'] >= 2, 'first number should be polled twice before being replaced');
|
||||
assert.ok(messages.includes('SUBMIT_PHONE_NUMBER'));
|
||||
@@ -1951,7 +1951,7 @@ test('phone verification helper honors timeout-window and poll-round settings be
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/did not succeed after 1 number replacements/i
|
||||
/更换 1 次号码后手机号验证仍未成功/
|
||||
);
|
||||
|
||||
assert.equal(messages.includes('RESEND_PHONE_VERIFICATION_CODE'), false);
|
||||
@@ -2051,7 +2051,7 @@ test('phone verification helper respects configured number replacement limit', a
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/did not succeed after 1 number replacements/i
|
||||
/更换 1 次号码后手机号验证仍未成功/
|
||||
);
|
||||
|
||||
const actions = requests.map((requestUrl) => requestUrl.searchParams.get('action'));
|
||||
@@ -4110,3 +4110,205 @@ test('phone verification helper logs no-supply diagnostics with consecutive stre
|
||||
assert.equal(currentState.phoneNoSupplyFailureStreak, 2);
|
||||
assert.equal(requests.some((entry) => entry.searchParams.get('action') === 'getNumber'), true);
|
||||
});
|
||||
|
||||
test('phone verification helper routes 5sim buy, check, and finish by current activation provider', async () => {
|
||||
const requests = [];
|
||||
let currentState = {
|
||||
phoneSmsProvider: '5sim',
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'england',
|
||||
fiveSimCountryLabel: 'England',
|
||||
fiveSimMaxPrice: '12',
|
||||
fiveSimOperator: 'any',
|
||||
verificationResendCount: 0,
|
||||
phoneVerificationReplacementLimit: 2,
|
||||
phoneCodeWaitSeconds: 60,
|
||||
phoneCodeTimeoutWindows: 1,
|
||||
phoneCodePollIntervalSeconds: 1,
|
||||
phoneCodePollMaxRounds: 1,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
|
||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push({ url: parsedUrl, options });
|
||||
if (parsedUrl.pathname === '/v1/guest/products/england/any') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ openai: { Category: 'activation', Qty: 3, Price: 9.5 } }),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/guest/prices') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ england: { any: { openai: { cost: 9.5, count: 3 } } } }),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/buy/activation/england/any/openai') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ id: 5001, phone: '+447911223344', country: 'england', operator: 'any', status: 'PENDING' }),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/check/5001') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ id: 5001, phone: '+447911223344', status: 'RECEIVED', sms: [{ text: 'OpenAI code 123456' }] }),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/finish/5001') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ status: 'FINISHED' }),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected 5sim path: ${parsedUrl.pathname}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.equal(currentState.currentPhoneActivation, null);
|
||||
assert.equal(currentState.reusablePhoneActivation.provider, '5sim');
|
||||
assert.equal(currentState.reusablePhoneActivation.activationId, '5001');
|
||||
assert.equal(currentState.reusablePhoneActivation.successfulUses, 1);
|
||||
const buy = requests.find((entry) => entry.url.pathname.includes('/buy/activation'));
|
||||
assert.equal(buy.url.searchParams.get('maxPrice'), '12');
|
||||
assert.equal(buy.url.searchParams.get('reuse'), '1');
|
||||
assert.equal(buy.options.headers.Authorization, 'Bearer demo-key');
|
||||
assert.deepStrictEqual(
|
||||
requests.map((entry) => entry.url.pathname),
|
||||
[
|
||||
'/v1/guest/products/england/any',
|
||||
'/v1/guest/prices',
|
||||
'/v1/user/buy/activation/england/any/openai',
|
||||
'/v1/user/check/5001',
|
||||
'/v1/user/finish/5001',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper routes 5sim reusable activation through reuse endpoint', async () => {
|
||||
const requests = [];
|
||||
let currentState = {
|
||||
phoneSmsProvider: '5sim',
|
||||
fiveSimApiKey: 'demo-key',
|
||||
fiveSimCountryId: 'england',
|
||||
fiveSimCountryLabel: 'England',
|
||||
fiveSimOperator: 'any',
|
||||
verificationResendCount: 0,
|
||||
phoneVerificationReplacementLimit: 2,
|
||||
phoneCodeWaitSeconds: 60,
|
||||
phoneCodeTimeoutWindows: 1,
|
||||
phoneCodePollIntervalSeconds: 1,
|
||||
phoneCodePollMaxRounds: 1,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: {
|
||||
activationId: '4001',
|
||||
phoneNumber: '+447911223344',
|
||||
provider: '5sim',
|
||||
serviceCode: 'openai',
|
||||
countryId: 'england',
|
||||
countryLabel: 'England',
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8');
|
||||
const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({});
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
createFiveSimProvider: fiveSimModule.createProvider,
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
if (parsedUrl.pathname === '/v1/user/reuse/openai/447911223344') {
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+447911223344', country: 'england', status: 'PENDING' }) };
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/check/4002') {
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+447911223344', status: 'RECEIVED', sms: [{ code: '654321' }] }) };
|
||||
}
|
||||
if (parsedUrl.pathname === '/v1/user/finish/4002') {
|
||||
return { ok: true, status: 200, text: async () => JSON.stringify({ status: 'FINISHED' }) };
|
||||
}
|
||||
throw new Error(`Unexpected 5sim path: ${parsedUrl.pathname}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.equal(currentState.reusablePhoneActivation.activationId, '4002');
|
||||
assert.equal(currentState.reusablePhoneActivation.successfulUses, 1);
|
||||
assert.deepStrictEqual(
|
||||
requests.map((url) => url.pathname),
|
||||
[
|
||||
'/v1/user/reuse/openai/447911223344',
|
||||
'/v1/user/check/4002',
|
||||
'/v1/user/finish/4002',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -200,6 +200,17 @@ const selectEmailGenerator = { value: 'duck' };
|
||||
const checkboxAutoDeleteIcloud = { checked: true };
|
||||
const selectIcloudHostPreference = { value: 'auto' };
|
||||
const inputPhoneVerificationEnabled = { checked: true };
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputHeroSmsReuseEnabled = { checked: true };
|
||||
const selectHeroSmsAcquirePriority = { value: 'country' };
|
||||
const inputHeroSmsMaxPrice = { value: '' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const inputPhoneReplacementLimit = { value: '3' };
|
||||
const inputPhoneCodeWaitSeconds = { value: '60' };
|
||||
const inputPhoneCodeTimeoutWindows = { value: '2' };
|
||||
const inputPhoneCodePollIntervalSeconds = { value: '5' };
|
||||
const inputPhoneCodePollMaxRounds = { value: '4' };
|
||||
const inputAccountRunHistoryTextEnabled = { checked: true };
|
||||
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
|
||||
const inputInbucketHost = { value: 'inbucket.local' };
|
||||
@@ -223,7 +234,33 @@ const inputAutoStepDelaySeconds = { value: '10' };
|
||||
const inputOAuthFlowTimeoutEnabled = { checked: true };
|
||||
const inputVerificationResendCount = { value: '6' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
|
||||
function getCloudflareDomainsFromState() { return { domains: ['example.com'], activeDomain: 'example.com' }; }
|
||||
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
|
||||
@@ -242,6 +279,27 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
|
||||
function normalizePhoneSmsProvider(value = '') { return String(value || '').trim().toLowerCase() === '5sim' ? '5sim' : 'hero-sms'; }
|
||||
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_FIVE_SIM_COUNTRY_LABEL) { return String(value || '').trim() || fallback; }
|
||||
function normalizeFiveSimOperator(value = '', fallback = DEFAULT_FIVE_SIM_OPERATOR) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimMaxPriceValue(value = '') { const numeric = Number(String(value ?? '').trim()); return Number.isFinite(numeric) && numeric > 0 ? String(Math.round(numeric * 10000) / 10000) : ''; }
|
||||
function normalizeFiveSimCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeFiveSimCountryId(entry?.id ?? entry, ''), label: String(entry?.label || entry?.id || entry || '').trim() })).filter((entry) => entry.id) : []; }
|
||||
function normalizeHeroSmsMaxPriceValue(value = '') { const numeric = Number(String(value ?? '').trim()); return Number.isFinite(numeric) && numeric > 0 ? String(Math.round(numeric * 10000) / 10000) : ''; }
|
||||
function normalizePhoneSmsMaxPriceValue(value = '', provider = getSelectedPhoneSmsProvider()) { return normalizePhoneSmsProvider(provider) === '5sim' ? normalizeFiveSimMaxPriceValue(value) : normalizeHeroSmsMaxPriceValue(value); }
|
||||
function normalizeHeroSmsReuseEnabledValue(value) { return value === undefined || value === null ? true : Boolean(value); }
|
||||
function normalizeHeroSmsAcquirePriority(value = '') { return String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country'; }
|
||||
function normalizeHeroSmsCountryId(value) { return Math.max(1, Math.floor(Number(value) || 52)); }
|
||||
function normalizeHeroSmsCountryLabel(value = '') { return String(value || '').trim() || 'Thailand'; }
|
||||
function normalizeHeroSmsCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeHeroSmsCountryId(entry?.id ?? entry), label: String(entry?.label || 'Thailand') })) : []; }
|
||||
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodeWaitSecondsValue(value, fallback = 60) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodeTimeoutWindowsValue(value, fallback = 2) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodePollIntervalSecondsValue(value, fallback = 5) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodePollMaxRoundsValue(value, fallback = 4) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function getSelectedHeroSmsCountryOption() { return { id: 52, label: 'Thailand' }; }
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }]; }
|
||||
${bundle}
|
||||
return {
|
||||
collectSettingsPayload,
|
||||
|
||||
@@ -178,6 +178,15 @@ return {
|
||||
assert.equal(api.getRunCountValue(), 3);
|
||||
});
|
||||
|
||||
test('sidepanel queues custom email pool refresh when the pool row is visible', () => {
|
||||
const source = extractFunction('updateMailProviderUI');
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/if \(useCustomEmailPool\) \{\s*syncRunCountFromCustomEmailPool\(\);\s*if \(typeof queueCustomEmailPoolRefresh === 'function'\) \{\s*queueCustomEmailPoolRefresh\(\);\s*\}\s*\}/
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => {
|
||||
const bundle = [
|
||||
extractFunction('getCustomVerificationPromptCopy'),
|
||||
|
||||
@@ -108,6 +108,8 @@ const selectIcloudFetchMode = { value: 'reuse_existing' };
|
||||
const selectIcloudTargetMailboxType = { value: 'forward-mailbox' };
|
||||
const selectIcloudForwardMailProvider = { value: 'gmail' };
|
||||
const inputPhoneVerificationEnabled = { checked: false };
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const inputAccountRunHistoryTextEnabled = { checked: false };
|
||||
const inputAccountRunHistoryHelperBaseUrl = { value: '' };
|
||||
const inputInbucketHost = { value: '' };
|
||||
@@ -131,25 +133,33 @@ const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputOAuthFlowTimeoutEnabled = { checked: true };
|
||||
const inputVerificationResendCount = { value: '4' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputHeroSmsReuseEnabled = { checked: true };
|
||||
const selectHeroSmsAcquirePriority = { value: 'country' };
|
||||
@@ -206,6 +216,20 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||
function normalizePhoneSmsProvider(value = '') { return String(value || '').trim().toLowerCase() === '5sim' ? '5sim' : 'hero-sms'; }
|
||||
function setPhoneSmsProviderSelectValue(provider) {
|
||||
const normalizedProvider = normalizePhoneSmsProvider(provider);
|
||||
selectPhoneSmsProvider.value = normalizedProvider;
|
||||
return normalizedProvider;
|
||||
}
|
||||
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_FIVE_SIM_COUNTRY_LABEL) { return String(value || '').trim() || fallback; }
|
||||
function normalizeFiveSimOperator(value = '', fallback = DEFAULT_FIVE_SIM_OPERATOR) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimMaxPriceValue(value = '') { const numeric = Number(String(value ?? '').trim()); return Number.isFinite(numeric) && numeric > 0 ? String(Math.round(numeric * 10000) / 10000) : ''; }
|
||||
function normalizeFiveSimCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeFiveSimCountryId(entry?.id ?? entry, ''), label: String(entry?.label || entry?.id || entry || '').trim() })).filter((entry) => entry.id) : []; }
|
||||
function normalizePhoneSmsMaxPriceValue(value = '', provider = getSelectedPhoneSmsProvider()) { return normalizePhoneSmsProvider(provider) === '5sim' ? normalizeFiveSimMaxPriceValue(value) : normalizeHeroSmsMaxPriceValue(value); }
|
||||
function normalizeHeroSmsCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeHeroSmsCountryId(entry?.id ?? entry), label: String(entry?.label || 'Thailand') })) : []; }
|
||||
function normalizeIcloudTargetMailboxType(value) { return String(value || '').trim().toLowerCase() === 'forward-mailbox' ? 'forward-mailbox' : 'icloud-inbox'; }
|
||||
function normalizeIcloudForwardMailProvider(value) { return String(value || '').trim().toLowerCase() === 'gmail' ? 'gmail' : 'qq'; }
|
||||
${bundle}
|
||||
@@ -379,16 +403,48 @@ const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputOAuthFlowTimeoutEnabled = { checked: true };
|
||||
const inputVerificationResendCount = { value: '' };
|
||||
const inputPhoneVerificationEnabled = { checked: false };
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputHeroSmsReuseEnabled = { checked: true };
|
||||
const selectHeroSmsAcquirePriority = { value: 'country' };
|
||||
const inputHeroSmsMaxPrice = { value: '' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const inputPhoneReplacementLimit = { value: '' };
|
||||
const inputPhoneCodeWaitSeconds = { value: '' };
|
||||
const inputPhoneCodeTimeoutWindows = { value: '' };
|
||||
const inputPhoneCodePollIntervalSeconds = { value: '' };
|
||||
const inputPhoneCodePollMaxRounds = { value: '' };
|
||||
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
|
||||
const inputRunCount = { value: '' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
function syncLatestState(state) { latestState = { ...latestState, ...state }; }
|
||||
function syncAutoRunState() {}
|
||||
function syncPasswordField() {}
|
||||
@@ -418,8 +474,29 @@ function normalizePhoneVerificationReplacementLimit(value, fallback = 3) {
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.max(PHONE_REPLACEMENT_LIMIT_MIN, Math.min(PHONE_REPLACEMENT_LIMIT_MAX, Math.floor(numeric)));
|
||||
}
|
||||
function normalizePhoneSmsProvider(value = '') { return String(value || '').trim().toLowerCase() === '5sim' ? '5sim' : 'hero-sms'; }
|
||||
function setPhoneSmsProviderSelectValue(provider) {
|
||||
const normalizedProvider = normalizePhoneSmsProvider(provider);
|
||||
selectPhoneSmsProvider.value = normalizedProvider;
|
||||
return normalizedProvider;
|
||||
}
|
||||
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_FIVE_SIM_COUNTRY_LABEL) { return String(value || '').trim() || fallback; }
|
||||
function normalizeFiveSimOperator(value = '', fallback = DEFAULT_FIVE_SIM_OPERATOR) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimMaxPriceValue(value = '') { const numeric = Number(String(value ?? '').trim()); return Number.isFinite(numeric) && numeric > 0 ? String(Math.round(numeric * 10000) / 10000) : ''; }
|
||||
function normalizeFiveSimCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeFiveSimCountryId(entry?.id ?? entry, ''), label: String(entry?.label || entry?.id || entry || '').trim() })).filter((entry) => entry.id) : []; }
|
||||
function normalizeHeroSmsCountryId() { return 52; }
|
||||
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
|
||||
function normalizeHeroSmsCountryLabel(value = '') { return String(value || '').trim() || 'Thailand'; }
|
||||
function normalizeHeroSmsCountryFallbackList(value = []) { return Array.isArray(value) ? value : []; }
|
||||
function normalizeHeroSmsReuseEnabledValue(value) { return value === undefined || value === null ? true : Boolean(value); }
|
||||
function normalizeHeroSmsAcquirePriority(value = '') { return String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country'; }
|
||||
function normalizePhoneCodeWaitSecondsValue(value, fallback = 60) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodeTimeoutWindowsValue(value, fallback = 2) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodePollIntervalSecondsValue(value, fallback = 5) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodePollMaxRoundsValue(value, fallback = 4) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function getSelectedHeroSmsCountryOption() { return { id: 52, label: 'Thailand' }; }
|
||||
function applyHeroSmsFallbackSelection() {}
|
||||
function updateHeroSmsPlatformDisplay() {}
|
||||
function applyAutoRunStatus() {}
|
||||
function markSettingsDirty() {}
|
||||
|
||||
@@ -178,6 +178,8 @@ const selectEmailGenerator = { value: 'duck' };
|
||||
const checkboxAutoDeleteIcloud = { checked: false };
|
||||
const selectIcloudHostPreference = { value: 'auto' };
|
||||
const inputPhoneVerificationEnabled = { checked: false };
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const inputAccountRunHistoryTextEnabled = { checked: false };
|
||||
const inputAccountRunHistoryHelperBaseUrl = { value: '' };
|
||||
const inputMail2925UseAccountPool = { checked: true };
|
||||
@@ -202,6 +204,32 @@ const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputOAuthFlowTimeoutEnabled = { checked: true };
|
||||
const inputVerificationResendCount = { value: '4' };
|
||||
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
|
||||
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
|
||||
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const PHONE_CODE_WAIT_SECONDS_MIN = 15;
|
||||
const PHONE_CODE_WAIT_SECONDS_MAX = 300;
|
||||
const DEFAULT_PHONE_CODE_WAIT_SECONDS = 60;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MIN = 1;
|
||||
const PHONE_CODE_TIMEOUT_WINDOWS_MAX = 10;
|
||||
const DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS = 2;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_INTERVAL_SECONDS_MAX = 30;
|
||||
const DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS = 5;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MIN = 1;
|
||||
const PHONE_CODE_POLL_MAX_ROUNDS_MAX = 120;
|
||||
const DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS = 4;
|
||||
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
|
||||
function getCloudflareDomainsFromState() {
|
||||
return { domains: [], activeDomain: '' };
|
||||
@@ -226,6 +254,27 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||
function normalizePhoneSmsProvider(value = '') { return String(value || '').trim().toLowerCase() === '5sim' ? '5sim' : 'hero-sms'; }
|
||||
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
|
||||
function normalizeFiveSimCountryId(value, fallback = DEFAULT_FIVE_SIM_COUNTRY_ID) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimCountryLabel(value = '', fallback = DEFAULT_FIVE_SIM_COUNTRY_LABEL) { return String(value || '').trim() || fallback; }
|
||||
function normalizeFiveSimOperator(value = '', fallback = DEFAULT_FIVE_SIM_OPERATOR) { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || fallback; }
|
||||
function normalizeFiveSimMaxPriceValue(value = '') { const numeric = Number(String(value ?? '').trim()); return Number.isFinite(numeric) && numeric > 0 ? String(Math.round(numeric * 10000) / 10000) : ''; }
|
||||
function normalizeFiveSimCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeFiveSimCountryId(entry?.id ?? entry, ''), label: String(entry?.label || entry?.id || entry || '').trim() })).filter((entry) => entry.id) : []; }
|
||||
function normalizeHeroSmsMaxPriceValue(value = '') { const numeric = Number(String(value ?? '').trim()); return Number.isFinite(numeric) && numeric > 0 ? String(Math.round(numeric * 10000) / 10000) : ''; }
|
||||
function normalizePhoneSmsMaxPriceValue(value = '', provider = getSelectedPhoneSmsProvider()) { return normalizePhoneSmsProvider(provider) === '5sim' ? normalizeFiveSimMaxPriceValue(value) : normalizeHeroSmsMaxPriceValue(value); }
|
||||
function normalizeHeroSmsReuseEnabledValue(value) { return value === undefined || value === null ? true : Boolean(value); }
|
||||
function normalizeHeroSmsAcquirePriority(value = '') { return String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country'; }
|
||||
function normalizeHeroSmsCountryId(value) { return Math.max(1, Math.floor(Number(value) || 52)); }
|
||||
function normalizeHeroSmsCountryLabel(value = '') { return String(value || '').trim() || 'Thailand'; }
|
||||
function normalizeHeroSmsCountryFallbackList(value = []) { return Array.isArray(value) ? value.map((entry) => ({ id: normalizeHeroSmsCountryId(entry?.id ?? entry), label: String(entry?.label || 'Thailand') })) : []; }
|
||||
function normalizePhoneVerificationReplacementLimit(value, fallback = 3) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodeWaitSecondsValue(value, fallback = 60) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodeTimeoutWindowsValue(value, fallback = 2) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodePollIntervalSecondsValue(value, fallback = 5) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function normalizePhoneCodePollMaxRoundsValue(value, fallback = 4) { const parsed = Number.parseInt(String(value ?? '').trim(), 10); return Number.isFinite(parsed) ? parsed : fallback; }
|
||||
function getSelectedHeroSmsCountryOption() { return { id: 52, label: 'Thailand' }; }
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }]; }
|
||||
${bundle}
|
||||
return { collectSettingsPayload };
|
||||
`)(normalizeIcloudTargetMailboxType, normalizeIcloudForwardMailProvider);
|
||||
|
||||
@@ -52,7 +52,7 @@ function extractFunction(name) {
|
||||
return sidepanelSource.slice(start, end);
|
||||
}
|
||||
|
||||
test('sidepanel html exposes phone verification toggle and dedicated HeroSMS rows', () => {
|
||||
test('sidepanel html exposes phone verification toggle and multi-provider SMS rows', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
assert.match(html, /id="row-phone-verification-enabled"/);
|
||||
@@ -67,6 +67,12 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
|
||||
assert.match(html, /id="row-phone-sms-provider-order-actions"/);
|
||||
assert.match(html, /id="btn-phone-sms-provider-order-reset"/);
|
||||
assert.match(html, /id="row-hero-sms-platform"/);
|
||||
assert.match(html, /id="select-phone-sms-provider"/);
|
||||
assert.match(html, /\.\.\/phone-sms\/providers\/hero-sms\.js/);
|
||||
assert.match(html, /\.\.\/phone-sms\/providers\/five-sim\.js/);
|
||||
assert.match(html, /\.\.\/phone-sms\/providers\/registry\.js/);
|
||||
assert.match(html, /<option value="hero-sms">HeroSMS<\/option>/);
|
||||
assert.match(html, /<option value="5sim">5sim<\/option>/);
|
||||
assert.match(html, /id="row-hero-sms-country"/);
|
||||
assert.match(html, /id="row-hero-sms-country-fallback"/);
|
||||
assert.match(html, /id="row-hero-sms-acquire-priority"/);
|
||||
@@ -75,6 +81,10 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
|
||||
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
|
||||
assert.match(html, /id="row-hero-sms-api-key"/);
|
||||
assert.match(html, /id="row-hero-sms-max-price"/);
|
||||
assert.match(html, /id="btn-phone-sms-balance"/);
|
||||
assert.match(html, /id="display-phone-sms-balance"/);
|
||||
assert.match(html, /id="row-five-sim-operator"/);
|
||||
assert.match(html, /id="input-five-sim-operator"/);
|
||||
assert.match(html, /id="row-hero-sms-current-number"/);
|
||||
assert.match(html, /id="row-hero-sms-current-countdown"/);
|
||||
assert.match(html, /id="row-hero-sms-price-tiers"/);
|
||||
@@ -107,7 +117,7 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
|
||||
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
||||
});
|
||||
|
||||
test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch', () => {
|
||||
test('updatePhoneVerificationSettingsUI toggles SMS rows from the sms switch and provider selection', () => {
|
||||
const api = new Function(`
|
||||
const phoneVerificationSectionExpanded = true;
|
||||
let latestState = {};
|
||||
@@ -159,6 +169,7 @@ const rowHeroSmsCountryFallback = { style: { display: 'none' } };
|
||||
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
|
||||
const rowHeroSmsApiKey = { style: { display: 'none' } };
|
||||
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
|
||||
const rowFiveSimOperator = { style: { display: 'none' } };
|
||||
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
|
||||
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
|
||||
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
|
||||
@@ -170,15 +181,12 @@ const rowPhoneCodeWaitSeconds = { style: { display: 'none' } };
|
||||
const rowPhoneCodeTimeoutWindows = { style: { display: 'none' } };
|
||||
const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } };
|
||||
const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
|
||||
const rowFiveSimApiKey = { style: { display: 'none' } };
|
||||
const rowFiveSimCountry = { style: { display: 'none' } };
|
||||
const rowFiveSimCountryFallback = { style: { display: 'none' } };
|
||||
const rowFiveSimOperator = { style: { display: 'none' } };
|
||||
const rowFiveSimProduct = { style: { display: 'none' } };
|
||||
const rowNexSmsApiKey = { style: { display: 'none' } };
|
||||
const rowNexSmsCountry = { style: { display: 'none' } };
|
||||
const rowNexSmsCountryFallback = { style: { display: 'none' } };
|
||||
const rowNexSmsServiceCode = { style: { display: 'none' } };
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
let selectedPhoneSmsProvider = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
function getSelectedPhoneSmsProvider() { return selectedPhoneSmsProvider; }
|
||||
function isFiveSimProviderSelected() { return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM; }
|
||||
function updateHeroSmsPlatformDisplay() {}
|
||||
|
||||
${extractFunction('updatePhoneVerificationSettingsUI')}
|
||||
|
||||
@@ -198,6 +206,7 @@ return {
|
||||
rowHeroSmsAcquirePriority,
|
||||
rowHeroSmsApiKey,
|
||||
rowHeroSmsMaxPrice,
|
||||
rowFiveSimOperator,
|
||||
rowHeroSmsCurrentNumber,
|
||||
rowHeroSmsCurrentCountdown,
|
||||
rowHeroSmsPriceTiers,
|
||||
@@ -209,15 +218,7 @@ return {
|
||||
rowPhoneCodeTimeoutWindows,
|
||||
rowPhoneCodePollIntervalSeconds,
|
||||
rowPhoneCodePollMaxRounds,
|
||||
rowFiveSimApiKey,
|
||||
rowFiveSimCountry,
|
||||
rowFiveSimCountryFallback,
|
||||
rowFiveSimOperator,
|
||||
rowFiveSimProduct,
|
||||
rowNexSmsApiKey,
|
||||
rowNexSmsCountry,
|
||||
rowNexSmsCountryFallback,
|
||||
rowNexSmsServiceCode,
|
||||
setSelectedPhoneSmsProvider(value) { selectedPhoneSmsProvider = value; },
|
||||
updatePhoneVerificationSettingsUI,
|
||||
};
|
||||
`)();
|
||||
@@ -230,12 +231,13 @@ return {
|
||||
assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, 'none');
|
||||
assert.equal(api.btnTogglePhoneVerificationSection.disabled, true);
|
||||
assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置');
|
||||
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsPlatform.style.display, '');
|
||||
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimOperator.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCurrentNumber.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
|
||||
@@ -271,6 +273,7 @@ return {
|
||||
assert.equal(api.rowHeroSmsAcquirePriority.style.display, '');
|
||||
assert.equal(api.rowHeroSmsApiKey.style.display, '');
|
||||
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
|
||||
assert.equal(api.rowFiveSimOperator.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
|
||||
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, '');
|
||||
assert.equal(api.rowHeroSmsPriceTiers.style.display, 'none');
|
||||
@@ -282,41 +285,10 @@ return {
|
||||
assert.equal(api.rowPhoneCodeTimeoutWindows.style.display, '');
|
||||
assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, '');
|
||||
assert.equal(api.rowPhoneCodePollMaxRounds.style.display, '');
|
||||
assert.equal(api.rowFiveSimApiKey.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimCountry.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimCountryFallback.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimOperator.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimProduct.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsApiKey.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
|
||||
|
||||
api.selectPhoneSmsProvider.value = '5sim';
|
||||
api.setLatestState({ phoneSmsProviderOrder: ['5sim'] });
|
||||
api.setSelectedPhoneSmsProvider('5sim');
|
||||
api.updatePhoneVerificationSettingsUI();
|
||||
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimApiKey.style.display, '');
|
||||
assert.equal(api.rowFiveSimCountry.style.display, '');
|
||||
assert.equal(api.rowFiveSimCountryFallback.style.display, '');
|
||||
assert.equal(api.rowFiveSimOperator.style.display, '');
|
||||
assert.equal(api.rowFiveSimProduct.style.display, '');
|
||||
assert.equal(api.rowNexSmsApiKey.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
|
||||
|
||||
api.selectPhoneSmsProvider.value = 'nexsms';
|
||||
api.setLatestState({ phoneSmsProviderOrder: ['nexsms'] });
|
||||
api.updatePhoneVerificationSettingsUI();
|
||||
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimApiKey.style.display, 'none');
|
||||
assert.equal(api.rowFiveSimCountry.style.display, 'none');
|
||||
assert.equal(api.rowNexSmsApiKey.style.display, '');
|
||||
assert.equal(api.rowNexSmsCountry.style.display, '');
|
||||
assert.equal(api.rowNexSmsCountryFallback.style.display, '');
|
||||
assert.equal(api.rowNexSmsServiceCode.style.display, '');
|
||||
});
|
||||
|
||||
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
|
||||
@@ -386,6 +358,7 @@ function getSelectedPhonePreferredActivation() {
|
||||
};
|
||||
}
|
||||
const inputHeroSmsMaxPrice = { value: '0.12' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const inputPhoneReplacementLimit = { value: '5' };
|
||||
const inputPhoneCodeWaitSeconds = { value: '75' };
|
||||
const inputPhoneCodeTimeoutWindows = { value: '3' };
|
||||
@@ -414,6 +387,12 @@ const PHONE_REPLACEMENT_LIMIT_MIN = 1;
|
||||
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const selectHeroSmsCountry = {
|
||||
value: '52',
|
||||
selectedIndex: 0,
|
||||
@@ -436,6 +415,14 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
|
||||
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
|
||||
${extractFunction('normalizePhoneSmsProvider')}
|
||||
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
|
||||
${extractFunction('normalizeFiveSimCountryId')}
|
||||
${extractFunction('normalizeFiveSimCountryLabel')}
|
||||
${extractFunction('normalizeFiveSimOperator')}
|
||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||
${extractFunction('normalizeFiveSimCountryFallbackList')}
|
||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||
${extractFunction('normalizePhoneVerificationReplacementLimit')}
|
||||
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
|
||||
@@ -485,6 +472,193 @@ return { collectSettingsPayload };
|
||||
assert.equal(payload.heroSmsCountryId, 52);
|
||||
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
|
||||
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
|
||||
assert.equal(payload.fiveSimApiKey, '');
|
||||
assert.equal(payload.fiveSimCountryId, 'england');
|
||||
});
|
||||
|
||||
test('switchPhoneSmsProvider saves API keys independently when the select value has already changed', async () => {
|
||||
const api = new Function(`
|
||||
let latestState = {
|
||||
phoneSmsProvider: 'hero-sms',
|
||||
heroSmsApiKey: 'hero-old',
|
||||
fiveSimApiKey: 'five-old',
|
||||
heroSmsMaxPrice: '0.11',
|
||||
fiveSimMaxPrice: '12',
|
||||
heroSmsCountryId: 52,
|
||||
heroSmsCountryLabel: 'Thailand',
|
||||
heroSmsCountryFallback: [],
|
||||
fiveSimCountryId: 'england',
|
||||
fiveSimCountryLabel: '英国 (England)',
|
||||
fiveSimCountryFallback: [],
|
||||
fiveSimOperator: 'any',
|
||||
};
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
|
||||
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
|
||||
const inputHeroSmsApiKey = { value: 'hero-live' };
|
||||
const inputHeroSmsMaxPrice = { value: '0.22' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const displayHeroSmsPriceTiers = { textContent: '' };
|
||||
const displayPhoneSmsBalance = { textContent: '' };
|
||||
const rowHeroSmsPriceTiers = { style: { display: '' } };
|
||||
let heroSmsCountrySelectionOrder = [];
|
||||
let savedPayload = null;
|
||||
|
||||
${extractFunction('normalizePhoneSmsProvider')}
|
||||
${extractFunction('setPhoneSmsProviderSelectValue')}
|
||||
${extractFunction('getLastAppliedPhoneSmsProvider')}
|
||||
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
|
||||
${extractFunction('normalizeFiveSimCountryId')}
|
||||
${extractFunction('normalizeFiveSimCountryLabel')}
|
||||
${extractFunction('normalizeFiveSimOperator')}
|
||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsCountryId')}
|
||||
${extractFunction('normalizeHeroSmsCountryLabel')}
|
||||
${extractFunction('normalizeHeroSmsCountryFallbackList')}
|
||||
${extractFunction('normalizeFiveSimCountryFallbackList')}
|
||||
function getSelectedHeroSmsCountryOption() {
|
||||
return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? { id: latestState.fiveSimCountryId || DEFAULT_FIVE_SIM_COUNTRY_ID, label: latestState.fiveSimCountryLabel || DEFAULT_FIVE_SIM_COUNTRY_LABEL }
|
||||
: { id: latestState.heroSmsCountryId || DEFAULT_HERO_SMS_COUNTRY_ID, label: latestState.heroSmsCountryLabel || DEFAULT_HERO_SMS_COUNTRY_LABEL };
|
||||
}
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() {
|
||||
return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? [{ id: 'england', label: '英国 (England)' }]
|
||||
: [{ id: 52, label: 'Thailand' }];
|
||||
}
|
||||
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
|
||||
function loadHeroSmsCountries() { return Promise.resolve(); }
|
||||
function applyHeroSmsFallbackSelection() {}
|
||||
function updatePhoneVerificationSettingsUI() {}
|
||||
function markSettingsDirty() {}
|
||||
function saveSettings() { savedPayload = { ...latestState }; return Promise.resolve(); }
|
||||
|
||||
${extractFunction('switchPhoneSmsProvider')}
|
||||
|
||||
return {
|
||||
selectPhoneSmsProvider,
|
||||
inputHeroSmsApiKey,
|
||||
get latestState() { return latestState; },
|
||||
get savedPayload() { return savedPayload; },
|
||||
switchPhoneSmsProvider,
|
||||
};
|
||||
`)();
|
||||
|
||||
// Browser change events update <select>.value before the listener runs.
|
||||
api.selectPhoneSmsProvider.value = '5sim';
|
||||
await api.switchPhoneSmsProvider(api.selectPhoneSmsProvider.value);
|
||||
|
||||
assert.equal(api.latestState.phoneSmsProvider, '5sim');
|
||||
assert.equal(api.latestState.heroSmsApiKey, 'hero-live');
|
||||
assert.equal(api.latestState.fiveSimApiKey, 'five-old');
|
||||
assert.equal(api.inputHeroSmsApiKey.value, 'five-old');
|
||||
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, '5sim');
|
||||
|
||||
api.inputHeroSmsApiKey.value = 'five-live';
|
||||
api.selectPhoneSmsProvider.value = 'hero-sms';
|
||||
await api.switchPhoneSmsProvider(api.selectPhoneSmsProvider.value);
|
||||
|
||||
assert.equal(api.latestState.phoneSmsProvider, 'hero-sms');
|
||||
assert.equal(api.latestState.heroSmsApiKey, 'hero-live');
|
||||
assert.equal(api.latestState.fiveSimApiKey, 'five-live');
|
||||
assert.equal(api.inputHeroSmsApiKey.value, 'hero-live');
|
||||
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, 'hero-sms');
|
||||
assert.equal(api.savedPayload.heroSmsApiKey, 'hero-live');
|
||||
assert.equal(api.savedPayload.fiveSimApiKey, 'five-live');
|
||||
});
|
||||
|
||||
test('previewHeroSmsPriceTiers prefers 5sim products price for buy-compatible any operator', async () => {
|
||||
const api = new Function(`
|
||||
let latestState = { phoneSmsProvider: '5sim', fiveSimOperator: 'any' };
|
||||
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
|
||||
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const inputHeroSmsMaxPrice = { value: '' };
|
||||
const inputHeroSmsApiKey = { value: '' };
|
||||
const inputFiveSimOperator = { value: 'any' };
|
||||
const displayHeroSmsPriceTiers = { textContent: '' };
|
||||
const rowHeroSmsPriceTiers = { style: { display: 'none' } };
|
||||
const fetchCalls = [];
|
||||
|
||||
${extractFunction('normalizePhoneSmsProvider')}
|
||||
function getSelectedPhoneSmsProvider() { return '5sim'; }
|
||||
${extractFunction('normalizeFiveSimCountryId')}
|
||||
${extractFunction('normalizeFiveSimCountryLabel')}
|
||||
${extractFunction('normalizeFiveSimOperator')}
|
||||
${extractFunction('normalizePhoneSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeFiveSimMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsMaxPriceValue')}
|
||||
${extractFunction('normalizeHeroSmsPriceForPreview')}
|
||||
${extractFunction('formatHeroSmsPriceForPreview')}
|
||||
${extractFunction('isHeroSmsPreviewEmptyPayload')}
|
||||
${extractFunction('collectHeroSmsPriceEntriesForPreview')}
|
||||
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
|
||||
${extractFunction('describeHeroSmsPreviewPayload')}
|
||||
${extractFunction('summarizeHeroSmsPreviewError')}
|
||||
function normalizeHeroSmsFetchErrorMessage(error) { return error?.message || String(error); }
|
||||
function syncHeroSmsFallbackSelectionOrderFromSelect() {
|
||||
return [{ id: 'vietnam', label: '越南 (Vietnam)' }];
|
||||
}
|
||||
function getSelectedHeroSmsCountryOption() {
|
||||
return { id: 'vietnam', label: '越南 (Vietnam)' };
|
||||
}
|
||||
function normalizePhoneSmsCountryId(value) { return normalizeFiveSimCountryId(value); }
|
||||
function normalizePhoneSmsCountryLabel(value) { return normalizeFiveSimCountryLabel(value); }
|
||||
function getHeroSmsCountryLabelById() { return '越南 (Vietnam)'; }
|
||||
async function fetch(url, options = {}) {
|
||||
const parsed = new URL(url);
|
||||
fetchCalls.push({ url: parsed, options });
|
||||
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ openai: { Category: 'activation', Qty: 4609, Price: 0.08 } }),
|
||||
};
|
||||
}
|
||||
if (parsed.pathname === '/v1/guest/prices') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({
|
||||
vietnam: {
|
||||
openai: {
|
||||
virtual21: { cost: 0.0769, count: 0 },
|
||||
virtual47: { cost: 0.1282, count: 4608 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
throw new Error('unexpected ' + parsed.pathname);
|
||||
}
|
||||
|
||||
${extractFunction('previewHeroSmsPriceTiers')}
|
||||
|
||||
return {
|
||||
displayHeroSmsPriceTiers,
|
||||
rowHeroSmsPriceTiers,
|
||||
fetchCalls,
|
||||
previewHeroSmsPriceTiers,
|
||||
};
|
||||
`)();
|
||||
|
||||
await api.previewHeroSmsPriceTiers();
|
||||
|
||||
assert.equal(api.displayHeroSmsPriceTiers.textContent, '越南 (Vietnam): 最低 0.08');
|
||||
assert.equal(api.rowHeroSmsPriceTiers.style.display, '');
|
||||
assert.deepStrictEqual(
|
||||
api.fetchCalls.map((entry) => entry.url.pathname),
|
||||
['/v1/guest/products/vietnam/any', '/v1/guest/prices']
|
||||
);
|
||||
});
|
||||
|
||||
test('hero sms max price input does not auto-save partial typing states', () => {
|
||||
|
||||
+12
-9
@@ -43,7 +43,7 @@
|
||||
- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程
|
||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新
|
||||
- 展示一个单独的“接码”开关;开启后才展示 HeroSMS 的接码国家与 API Key 设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
|
||||
- 展示一个单独的“接码”开关与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim,开启后才展示所选平台的国家、API Key、价格上限等设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
|
||||
- 展示 `Plus 模式` 开关与 PayPal 账号池配置;开启后 PayPal 配置行会切换为“账号下拉框 + 添加按钮”,添加按钮复用公共表单弹窗录入账号和密码;步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
|
||||
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
- 2925 是否启用号池模式 `mail2925UseAccountPool`
|
||||
- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
|
||||
- Cloudflare / Temp Email 设置
|
||||
- 接码开关,以及 HeroSMS 的 API Key 与默认国家设置
|
||||
- 接码开关、接码平台 `phoneSmsProvider`,以及 HeroSMS / 5sim 各自的 API Key、国家/地区、价格上限和平台扩展设置
|
||||
- iCloud 相关偏好
|
||||
- LuckMail API 配置
|
||||
- 自动运行默认配置
|
||||
@@ -447,16 +447,19 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
|
||||
1. 监听 localhost callback
|
||||
2. 准备 OAuth 同意页;如果页面已进入 `add-phone / phone-verification`,先切入手机号验证共享流程
|
||||
3. 手机号验证共享流程会按当前 sidepanel 中保存的 HeroSMS 国家与 API Key 申请或复用号码、提交号码、轮询短信验证码,并在验证码被拒绝或长时间收不到短信时决定重发、换号或把自动流拉回 Step 7
|
||||
4. 手机号验证完成后,继续等待 OAuth 同意页出现
|
||||
5. 尝试多轮点击“继续”
|
||||
6. 一旦捕获 localhost callback,写入状态并完成步骤
|
||||
3. 手机号验证共享流程会按当前 sidepanel 中保存的接码平台 `phoneSmsProvider` 选择 HeroSMS 或 5sim,并使用对应平台的 API Key、国家/地区、价格上限申请或复用号码
|
||||
4. 后台提交号码后,会按 `currentPhoneActivation.provider` 分发后续查码、完成、取消、ban、reuse:HeroSMS 走原 `getStatus / setStatus` 逻辑,5sim 走 `/v1/user/check|finish|cancel|ban|reuse`
|
||||
5. 验证码被拒绝或长时间收不到短信时,会按平台决定重发、换号、ban/取消当前激活,必要时把自动流拉回 Step 7
|
||||
6. 手机号验证完成后,继续等待 OAuth 同意页出现
|
||||
7. 尝试多轮点击“继续”
|
||||
8. 一旦捕获 localhost callback,写入状态并完成步骤
|
||||
|
||||
补充:
|
||||
|
||||
- HeroSMS 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
|
||||
- 如果同一个号码在重发短信后 60 秒仍收不到验证码,后台会抛出“回到步骤 7 重新拿新号码”的恢复错误,而不是把当前号码无限重试下去。
|
||||
- Step 9 在等待 localhost callback 时会动态读取 `oauthFlowTimeoutEnabled`:开启时继续受 Step 7 后链总预算约束;关闭时仅保留本地 240 秒回调等待上限,不再因 5 分钟总预算触发回跳 Step 7。
|
||||
- HeroSMS 和 5sim 都归入接码平台适配层;默认平台是 HeroSMS,5sim 产品码固定为 `openai`,operator 默认 `any`。
|
||||
- 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
|
||||
- 5sim 的国家/地区使用字符串 slug(如 `england / usa / thailand`),HeroSMS 仍使用数字国家 ID。
|
||||
- 如果同一个号码在重发短信后仍收不到验证码,后台会按当前平台取消或 ban 激活并换号,而不是把当前号码无限重试下去。
|
||||
|
||||
### Step 10
|
||||
|
||||
|
||||
Reference in New Issue
Block a user