feat: add Luban SMS provider
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
SMSBower Phone Fill Tool
|
SMS/Luban Phone Fill Tool
|
||||||
|
|
||||||
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。点一次“获取并填入”就获取一个 SMSBower 手机号,自动填入当前页面最像手机号的输入框、复制到剪贴板,并自动点击页面上的下一步/发送验证码按钮。再次获取会先释放上一个激活,再拿新号替换页面输入框并自动提交。
|
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。支持 SMSBower 和 Luban SMS 两个接码平台;点一次“获取并填入”会获取手机号,自动填入当前页面最像手机号的输入框、复制到剪贴板,并自动点击页面上的下一步/发送验证码按钮。再次获取会先释放上一个激活,再拿新号替换页面输入框并自动提交。
|
||||||
|
|
||||||
本版已集成 FlowPilot 同款自动切号机制:
|
已集成功能:
|
||||||
|
- 接码平台可选:SMSBower / Luban SMS。
|
||||||
|
- SMSBower 使用 `api_key`、`getPricesV3`、`getNumberV2`、`getStatus`、`setStatus`。
|
||||||
|
- Luban SMS 使用 `apikey`、`getNumber`、`getSms`、`setStatus status=reject`。
|
||||||
- 悬浮窗打开后默认开启“自动切号:开”。
|
- 悬浮窗打开后默认开启“自动切号:开”。
|
||||||
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,只要确认是当前新号码触发的错误,插件会自动切换下一个号码;旧号按被拒原因释放,新号自动填入并复制。
|
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,只要确认是当前新号码触发的错误,插件会自动切换下一个号码。
|
||||||
- 页面提示“无法向此电话号码发送验证码”等发送失败时,插件会自动按可恢复失败换号。
|
- 页面提示“无法向此电话号码发送验证码”等发送失败时,插件会自动按可恢复失败换号。
|
||||||
- “检测报错换号”和“已使用换号”仍保留为手动兜底按钮。
|
- “检测报错换号”和“已使用换号”仍保留为手动兜底按钮。
|
||||||
- 填号后会自动寻找并点击目标页面上的“下一步/继续/发送验证码/提交”等按钮;如果找不到按钮,会尝试提交表单或发送 Enter。
|
- 填号后会自动寻找并点击目标页面上的“下一步/继续/发送验证码/提交”等按钮;如果找不到按钮,会尝试提交表单或发送 Enter。
|
||||||
@@ -17,22 +20,23 @@ SMSBower Phone Fill Tool
|
|||||||
|
|
||||||
使用:
|
使用:
|
||||||
1. 打开任意网页,先点一下你要填写的输入框。
|
1. 打开任意网页,先点一下你要填写的输入框。
|
||||||
2. 点浏览器工具栏里的“SMSBower 悬浮填号”扩展图标。
|
2. 点浏览器工具栏里的“接码填号”扩展图标。
|
||||||
3. 页面右上角会出现悬浮窗,可拖动。
|
3. 页面右上角会出现悬浮窗,可拖动。
|
||||||
4. 填 SMSBower API Key、项目代码、价格上下限、国家顺序。
|
4. 选择接码平台:
|
||||||
|
- SMSBower:填写 API Key、项目代码、价格上下限、国家顺序。
|
||||||
|
- Luban SMS:填写 API Key 和 `service_id`;`service_id` 可在 Luban 控制台或 API 服务列表中查询。Luban 的国家/服务由 `service_id` 决定,所以国家选择会隐藏。
|
||||||
5. 顶部“获取并填入”会立即拿号、填入、复制,并自动提交/点击发送验证码。
|
5. 顶部“获取并填入”会立即拿号、填入、复制,并自动提交/点击发送验证码。
|
||||||
6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。
|
6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。
|
||||||
7. 如果目标页面拒绝号码,保持悬浮窗打开即可:插件会自动扫描页面错误文本并切换下一个号码。
|
7. 如果目标页面拒绝号码,保持悬浮窗打开即可:插件会自动扫描页面错误文本并切换下一个号码。
|
||||||
- “自动切号:开/关”可以临时启停自动扫描;
|
- “自动切号:开/关”可以临时启停自动扫描;
|
||||||
- “检测报错换号”用于手动触发一次扫描;
|
- “检测报错换号”用于手动触发一次扫描;
|
||||||
- “已使用换号”用于你已经确认号码被使用时立即换号。
|
- “已使用换号”用于你已经确认号码被使用时立即换号。
|
||||||
8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;如果 SMSBower 已收到 STATUS_OK,会显示验证码并停止。点获取新号、保存、刷新国家或取消当前会停止这轮验证码轮询。
|
8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;收到验证码后显示并停止。
|
||||||
9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。
|
9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。
|
||||||
10. 国家搜索支持中文别名,例如搜“印度”会出现 India 和 Indonesia。
|
10. 点悬浮窗右上角 × 只是隐藏;再次点扩展图标会重新显示。
|
||||||
11. 点悬浮窗右上角 × 只是隐藏;再次点扩展图标会重新显示。
|
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
- “复制验证码”只查询当前 SMSBower 激活订单的 getStatus,不会自动填验证码。
|
- “复制验证码”只复制当前已查询到的验证码,不会自动填验证码。
|
||||||
- 拿号优先级:价格低优先;同价格按你选择国家的顺序优先。
|
- SMSBower 拿号优先级:价格低优先;同价格按你选择国家的顺序优先。
|
||||||
- SMSBower 默认接口地址为 https://smsbower.app/stubs/handler_api.php,可在悬浮窗里改。
|
- Luban SMS 官方文档基础地址为 `https://lubansms.com/v2/api/`,扩展会按接口自动拼接 `getNumber/getSms/setStatus/List`。
|
||||||
- 本地验证:`node --test tests/provider.test.js`。
|
- 本地验证:`node --test tests/provider.test.js`。
|
||||||
|
|||||||
+105
-68
@@ -1,7 +1,10 @@
|
|||||||
const DEFAULT_SETTINGS = Object.freeze({
|
const DEFAULT_SETTINGS = Object.freeze({
|
||||||
|
provider: 'sms-bower',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
baseUrl: 'https://smsbower.app/stubs/handler_api.php',
|
baseUrl: 'https://smsbower.app/stubs/handler_api.php',
|
||||||
|
lubanBaseUrl: 'https://lubansms.com/v2/api',
|
||||||
serviceCode: 'dr',
|
serviceCode: 'dr',
|
||||||
|
lubanServiceId: '',
|
||||||
minPrice: '',
|
minPrice: '',
|
||||||
maxPrice: '',
|
maxPrice: '',
|
||||||
countries: [{ id: 6, label: 'Indonesia' }],
|
countries: [{ id: 6, label: 'Indonesia' }],
|
||||||
@@ -18,7 +21,18 @@ const FALLBACK_COUNTRIES = Object.freeze([
|
|||||||
{ id: 151, label: 'Japan' }, { id: 187, label: 'USA' }
|
{ id: 151, label: 'Japan' }, { id: 187, label: 'USA' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const PROVIDERS = Object.freeze({
|
||||||
|
'sms-bower': {
|
||||||
|
id: 'sms-bower', name: 'SMSBower', apiKeyParam: 'api_key', baseUrlKey: 'baseUrl', defaultBaseUrl: 'https://smsbower.app/stubs/handler_api.php',
|
||||||
|
},
|
||||||
|
'luban-sms': {
|
||||||
|
id: 'luban-sms', name: 'Luban SMS', apiKeyParam: 'apikey', baseUrlKey: 'lubanBaseUrl', defaultBaseUrl: 'https://lubansms.com/v2/api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function asText(value = '', fallback = '') { return String(value ?? '').trim() || fallback; }
|
function asText(value = '', fallback = '') { return String(value ?? '').trim() || fallback; }
|
||||||
|
function normalizeProvider(value = '') { return PROVIDERS[value] ? value : DEFAULT_SETTINGS.provider; }
|
||||||
|
function getProvider(settings = {}) { return PROVIDERS[normalizeProvider(settings.provider)] || PROVIDERS[DEFAULT_SETTINGS.provider]; }
|
||||||
function normalizeCountryId(value, fallback = 6) {
|
function normalizeCountryId(value, fallback = 6) {
|
||||||
const parsed = Math.floor(Number(value));
|
const parsed = Math.floor(Number(value));
|
||||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||||
@@ -43,21 +57,23 @@ function isPhoneNumberDeliveryRefusedError(value) {
|
|||||||
}
|
}
|
||||||
function getPhoneReplacementReleaseAction(reason = '') {
|
function getPhoneReplacementReleaseAction(reason = '') {
|
||||||
const normalized = normalizeFailureReason(reason);
|
const normalized = normalizeFailureReason(reason);
|
||||||
if (normalized === 'code_rejected' || normalized === 'phone_number_used' || normalized === 'phone_number_in_use' || normalized === 'phone_max_usage_exceeded' || isPhoneNumberUsedError(reason)) {
|
if (normalized === 'code_rejected' || normalized === 'phone_number_used' || normalized === 'phone_number_in_use' || normalized === 'phone_max_usage_exceeded' || isPhoneNumberUsedError(reason)) return 'ban';
|
||||||
return 'ban';
|
|
||||||
}
|
|
||||||
return 'cancel';
|
return 'cancel';
|
||||||
}
|
}
|
||||||
function normalizeSettings(input = {}) {
|
function normalizeSettings(input = {}) {
|
||||||
const countries = Array.isArray(input.countries) && input.countries.length
|
const countries = Array.isArray(input.countries) && input.countries.length
|
||||||
? input.countries.map((c) => ({ id: normalizeCountryId(c.id ?? c.country, 6), label: asText(c.label, `Country #${normalizeCountryId(c.id ?? c.country, 6)}`) }))
|
? input.countries.map((c) => ({ id: normalizeCountryId(c.id ?? c.country, 6), label: asText(c.label, `Country #${normalizeCountryId(c.id ?? c.country, 6)}`) }))
|
||||||
: DEFAULT_SETTINGS.countries;
|
: DEFAULT_SETTINGS.countries;
|
||||||
|
const provider = normalizeProvider(input.provider);
|
||||||
return {
|
return {
|
||||||
...DEFAULT_SETTINGS,
|
...DEFAULT_SETTINGS,
|
||||||
...input,
|
...input,
|
||||||
|
provider,
|
||||||
apiKey: asText(input.apiKey),
|
apiKey: asText(input.apiKey),
|
||||||
baseUrl: asText(input.baseUrl, DEFAULT_SETTINGS.baseUrl),
|
baseUrl: asText(input.baseUrl, DEFAULT_SETTINGS.baseUrl),
|
||||||
|
lubanBaseUrl: asText(input.lubanBaseUrl, DEFAULT_SETTINGS.lubanBaseUrl),
|
||||||
serviceCode: asText(input.serviceCode, DEFAULT_SETTINGS.serviceCode).toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_SETTINGS.serviceCode,
|
serviceCode: asText(input.serviceCode, DEFAULT_SETTINGS.serviceCode).toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_SETTINGS.serviceCode,
|
||||||
|
lubanServiceId: asText(input.lubanServiceId ?? input.serviceId ?? input.luban_service_id, DEFAULT_SETTINGS.lubanServiceId).replace(/[^0-9a-zA-Z_-]+/g, ''),
|
||||||
minPrice: normalizePrice(input.minPrice) === null ? '' : String(normalizePrice(input.minPrice)),
|
minPrice: normalizePrice(input.minPrice) === null ? '' : String(normalizePrice(input.minPrice)),
|
||||||
maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)),
|
maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)),
|
||||||
countries,
|
countries,
|
||||||
@@ -73,20 +89,31 @@ async function saveSettings(update = {}) {
|
|||||||
await chrome.storage.local.set(next);
|
await chrome.storage.local.set(next);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
function providerBaseUrl(settings) {
|
||||||
|
const provider = getProvider(settings);
|
||||||
|
return asText(settings[provider.baseUrlKey], provider.defaultBaseUrl);
|
||||||
|
}
|
||||||
function buildUrl(settings, query = {}) {
|
function buildUrl(settings, query = {}) {
|
||||||
const url = new URL(settings.baseUrl || DEFAULT_SETTINGS.baseUrl);
|
const provider = getProvider(settings);
|
||||||
if (settings.apiKey) url.searchParams.set('api_key', settings.apiKey);
|
let base = providerBaseUrl(settings);
|
||||||
Object.entries(query).forEach(([k, v]) => {
|
const queryEntries = { ...query };
|
||||||
if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v));
|
if (provider.id === 'luban-sms') {
|
||||||
});
|
const endpoint = String(queryEntries.__endpoint || '').replace(/^\/+/, '');
|
||||||
|
delete queryEntries.__endpoint;
|
||||||
|
if (endpoint) {
|
||||||
|
const root = base.replace(/\/+(getBalance|getNumber|getSms|setStatus|getAgainNmuber|countries|List|smsHistory|getKeywordNumber|getKeywordSms|delKeywordNumber|keywordSmsHistory|bulksms)\/?$/i, '');
|
||||||
|
base = `${root.replace(/\/+$/, '')}/${endpoint}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = new URL(base);
|
||||||
|
if (settings.apiKey) url.searchParams.set(provider.apiKeyParam, settings.apiKey);
|
||||||
|
Object.entries(queryEntries).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v)); });
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
function parsePayload(text) {
|
function parsePayload(text) {
|
||||||
const t = String(text || '').trim();
|
const t = String(text || '').trim();
|
||||||
if (!t) return '';
|
if (!t) return '';
|
||||||
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) {
|
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) { try { return JSON.parse(t); } catch {} }
|
||||||
try { return JSON.parse(t); } catch {}
|
|
||||||
}
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
function describePayload(payload) {
|
function describePayload(payload) {
|
||||||
@@ -98,13 +125,15 @@ function describePayload(payload) {
|
|||||||
}
|
}
|
||||||
return String(payload || '').trim();
|
return String(payload || '').trim();
|
||||||
}
|
}
|
||||||
async function apiRequest(settings, query, label = 'SMSBower 请求') {
|
async function apiRequest(settings, query, label = '接码平台请求') {
|
||||||
if (!settings.apiKey) throw new Error('请先填写 SMSBower API Key。');
|
const provider = getProvider(settings);
|
||||||
|
if (!settings.apiKey) throw new Error(`请先填写 ${provider.name} API Key。`);
|
||||||
const response = await fetch(buildUrl(settings, query), { method: 'GET', headers: { Accept: 'application/json, text/plain, */*' } });
|
const response = await fetch(buildUrl(settings, query), { method: 'GET', headers: { Accept: 'application/json, text/plain, */*' } });
|
||||||
const payload = parsePayload(await response.text());
|
const payload = parsePayload(await response.text());
|
||||||
const message = describePayload(payload);
|
const message = describePayload(payload);
|
||||||
const apiStatus = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.status) : null;
|
const apiStatus = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.status) : null;
|
||||||
if (!response.ok || apiStatus === 0 || /^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) {
|
const code = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.code) : null;
|
||||||
|
if (!response.ok || apiStatus === 0 || (Number.isFinite(code) && code !== 0) || /^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) {
|
||||||
throw new Error(`${label}失败:${message || response.status}`);
|
throw new Error(`${label}失败:${message || response.status}`);
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
@@ -115,18 +144,16 @@ function normalizeActivation(payload, fallback = {}) {
|
|||||||
if (m) payload = { activationId: m[1], phoneNumber: m[2] };
|
if (m) payload = { activationId: m[1], phoneNumber: m[2] };
|
||||||
}
|
}
|
||||||
const src = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
const src = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
||||||
const activationId = asText(src.activationId ?? src.activation_id ?? src.id ?? fallback.activationId);
|
const activationId = asText(src.activationId ?? src.activation_id ?? src.request_id ?? src.id ?? fallback.activationId);
|
||||||
const phoneNumber = asText(src.phoneNumber ?? src.phone_number ?? src.phone ?? src.number ?? fallback.phoneNumber);
|
const phoneNumber = asText(src.phoneNumber ?? src.phone_number ?? src.phone ?? src.number ?? fallback.phoneNumber);
|
||||||
if (!activationId || !phoneNumber) return null;
|
if (!activationId || !phoneNumber) return null;
|
||||||
const countryId = normalizeCountryId(src.countryId ?? src.country_id ?? src.country ?? fallback.countryId, fallback.countryId ?? 6);
|
const countryId = normalizeCountryId(src.countryId ?? src.country_id ?? src.country ?? fallback.countryId, fallback.countryId ?? 6);
|
||||||
return {
|
return {
|
||||||
activationId,
|
activationId, phoneNumber, provider: fallback.provider || 'sms-bower',
|
||||||
phoneNumber,
|
serviceCode: asText(src.serviceCode ?? src.service_id ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode),
|
||||||
provider: 'sms-bower',
|
|
||||||
serviceCode: asText(src.serviceCode ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode),
|
|
||||||
countryId,
|
countryId,
|
||||||
countryLabel: asText(src.countryLabel ?? fallback.countryLabel, fallback.countryLabel || `Country #${countryId}`),
|
countryLabel: asText(src.countryLabel ?? src.country_name_zh ?? src.country_name_en ?? fallback.countryLabel, fallback.countryLabel || `Country #${countryId}`),
|
||||||
price: Number.isFinite(Number(src.price ?? fallback.price)) ? Number(src.price ?? fallback.price) : null,
|
price: Number.isFinite(Number(src.price ?? src.cost ?? fallback.price)) ? Number(src.price ?? src.cost ?? fallback.price) : null,
|
||||||
acquiredAt: Date.now(),
|
acquiredAt: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -154,7 +181,7 @@ function filterPriceEntries(entries, settings) {
|
|||||||
if (min !== null && max !== null && min > max) throw new Error(`价格区间无效:最低价 ${min} 高于最高价 ${max}。`);
|
if (min !== null && max !== null && min > max) throw new Error(`价格区间无效:最低价 ${min} 高于最高价 ${max}。`);
|
||||||
return entries.filter((e) => e.inStock && (min === null || e.cost >= min) && (max === null || e.cost <= max));
|
return entries.filter((e) => e.inStock && (min === null || e.cost >= min) && (max === null || e.cost <= max));
|
||||||
}
|
}
|
||||||
async function fetchPriceCandidates(settings) {
|
async function fetchSmsBowerCandidates(settings) {
|
||||||
const out = [];
|
const out = [];
|
||||||
for (let index = 0; index < settings.countries.length; index += 1) {
|
for (let index = 0; index < settings.countries.length; index += 1) {
|
||||||
const country = settings.countries[index];
|
const country = settings.countries[index];
|
||||||
@@ -162,46 +189,52 @@ async function fetchPriceCandidates(settings) {
|
|||||||
const payload = await apiRequest(settings, { action: 'getPricesV3', service: settings.serviceCode, country: country.id }, `查询价格 ${country.label}`);
|
const payload = await apiRequest(settings, { action: 'getPricesV3', service: settings.serviceCode, country: country.id }, `查询价格 ${country.label}`);
|
||||||
const prices = filterPriceEntries(collectPriceEntries(payload), settings);
|
const prices = filterPriceEntries(collectPriceEntries(payload), settings);
|
||||||
prices.forEach((entry) => out.push({ country, countryIndex: index, price: entry.cost, count: entry.count }));
|
prices.forEach((entry) => out.push({ country, countryIndex: index, price: entry.cost, count: entry.count }));
|
||||||
} catch (error) {
|
} catch (error) { out.push({ country, countryIndex: index, price: Number.POSITIVE_INFINITY, count: 0, error: error.message || String(error) }); }
|
||||||
out.push({ country, countryIndex: index, price: Number.POSITIVE_INFINITY, count: 0, error: error.message || String(error) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out
|
return out.filter((x) => Number.isFinite(x.price)).sort((a, b) => (a.price - b.price) || (a.countryIndex - b.countryIndex));
|
||||||
.filter((x) => Number.isFinite(x.price))
|
}
|
||||||
.sort((a, b) => (a.price - b.price) || (a.countryIndex - b.countryIndex));
|
async function fetchLubanCandidates(settings) {
|
||||||
|
if (!settings.lubanServiceId) throw new Error('请先填写 Luban SMS service_id。可在 Luban 控制台或服务列表接口查询。');
|
||||||
|
let price = null;
|
||||||
|
try {
|
||||||
|
const payload = await apiRequest(settings, { __endpoint: 'List', country: '', service: '', language: 'en', page: 1 }, '查询 Luban 服务列表');
|
||||||
|
const list = Array.isArray(payload?.msg) ? payload.msg : [];
|
||||||
|
const found = list.find((x) => String(x.service_id) === String(settings.lubanServiceId));
|
||||||
|
if (found && Number.isFinite(Number(found.cost))) price = Number(found.cost);
|
||||||
|
} catch {}
|
||||||
|
if (normalizePrice(settings.minPrice) !== null && price !== null && price < normalizePrice(settings.minPrice)) return [];
|
||||||
|
if (normalizePrice(settings.maxPrice) !== null && price !== null && price > normalizePrice(settings.maxPrice)) return [];
|
||||||
|
return [{ country: { id: -1, label: 'Luban service' }, countryIndex: 0, price: price ?? 0, count: 1, serviceId: settings.lubanServiceId }];
|
||||||
|
}
|
||||||
|
async function fetchPriceCandidates(settings) {
|
||||||
|
return getProvider(settings).id === 'luban-sms' ? fetchLubanCandidates(settings) : fetchSmsBowerCandidates(settings);
|
||||||
}
|
}
|
||||||
async function releaseActivation(settings, activation, releaseAction = 'cancel', quiet = false) {
|
async function releaseActivation(settings, activation, releaseAction = 'cancel', quiet = false) {
|
||||||
if (!activation?.activationId) return { ok: false, skipped: true, releaseAction };
|
if (!activation?.activationId) return { ok: false, skipped: true, releaseAction };
|
||||||
|
const provider = getProvider(settings);
|
||||||
const normalizedAction = String(releaseAction || '').trim().toLowerCase() === 'ban' ? 'ban' : 'cancel';
|
const normalizedAction = String(releaseAction || '').trim().toLowerCase() === 'ban' ? 'ban' : 'cancel';
|
||||||
try {
|
try {
|
||||||
// SMSBower uses the same lifecycle endpoint/status for cancel-style release here.
|
let payload;
|
||||||
const payload = await apiRequest(settings, { action: 'setStatus', status: 8, id: activation.activationId }, normalizedAction === 'ban' ? '释放被拒号码' : '取消上一个号码激活');
|
if (provider.id === 'luban-sms') payload = await apiRequest(settings, { __endpoint: 'setStatus', request_id: activation.activationId, status: 'reject' }, '释放 Luban 号码');
|
||||||
|
else payload = await apiRequest(settings, { action: 'setStatus', status: 8, id: activation.activationId }, normalizedAction === 'ban' ? '释放被拒号码' : '取消上一个号码激活');
|
||||||
return { ok: true, payload: describePayload(payload), releaseAction: normalizedAction, activationId: activation.activationId };
|
return { ok: true, payload: describePayload(payload), releaseAction: normalizedAction, activationId: activation.activationId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!quiet) throw error;
|
if (!quiet) throw error;
|
||||||
return { ok: false, error: error.message || String(error), releaseAction: normalizedAction, activationId: activation.activationId };
|
return { ok: false, error: error.message || String(error), releaseAction: normalizedAction, activationId: activation.activationId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function cancelActivation(settings, activation, quiet = false) {
|
async function cancelActivation(settings, activation, quiet = false) { return releaseActivation(settings, activation, 'cancel', quiet); }
|
||||||
return releaseActivation(settings, activation, 'cancel', quiet);
|
async function banActivation(settings, activation, quiet = false) { return releaseActivation(settings, activation, 'ban', quiet); }
|
||||||
}
|
|
||||||
async function banActivation(settings, activation, quiet = false) {
|
|
||||||
return releaseActivation(settings, activation, 'ban', quiet);
|
|
||||||
}
|
|
||||||
async function requestActivation(settings, candidate) {
|
async function requestActivation(settings, candidate) {
|
||||||
const payload = await apiRequest(settings, {
|
const provider = getProvider(settings);
|
||||||
action: 'getNumberV2',
|
if (provider.id === 'luban-sms') {
|
||||||
service: settings.serviceCode,
|
const payload = await apiRequest(settings, { __endpoint: 'getNumber', service_id: candidate.serviceId || settings.lubanServiceId }, '购买 Luban 手机号');
|
||||||
country: candidate.country.id,
|
const activation = normalizeActivation(payload, { provider: provider.id, serviceCode: settings.lubanServiceId, price: candidate.price, countryId: -1, countryLabel: 'Luban service' });
|
||||||
minPrice: normalizePrice(settings.minPrice),
|
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
|
||||||
maxPrice: Number.isFinite(candidate.price) ? candidate.price : normalizePrice(settings.maxPrice),
|
return activation;
|
||||||
}, `购买手机号 ${candidate.country.label}`);
|
}
|
||||||
const activation = normalizeActivation(payload, {
|
const payload = await apiRequest(settings, { action: 'getNumberV2', service: settings.serviceCode, country: candidate.country.id, minPrice: normalizePrice(settings.minPrice), maxPrice: Number.isFinite(candidate.price) ? candidate.price : normalizePrice(settings.maxPrice) }, `购买手机号 ${candidate.country.label}`);
|
||||||
countryId: candidate.country.id,
|
const activation = normalizeActivation(payload, { provider: provider.id, countryId: candidate.country.id, countryLabel: candidate.country.label, serviceCode: settings.serviceCode, price: candidate.price });
|
||||||
countryLabel: candidate.country.label,
|
|
||||||
serviceCode: settings.serviceCode,
|
|
||||||
price: candidate.price,
|
|
||||||
});
|
|
||||||
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
|
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
|
||||||
return activation;
|
return activation;
|
||||||
}
|
}
|
||||||
@@ -212,16 +245,14 @@ async function fetchNextPhone(options = {}) {
|
|||||||
const releaseAction = options.releaseAction || getPhoneReplacementReleaseAction(reason);
|
const releaseAction = options.releaseAction || getPhoneReplacementReleaseAction(reason);
|
||||||
const releasePrevious = previous ? await releaseActivation(settings, previous, releaseAction, true) : { ok: false, skipped: true, releaseAction };
|
const releasePrevious = previous ? await releaseActivation(settings, previous, releaseAction, true) : { ok: false, skipped: true, releaseAction };
|
||||||
const candidates = await fetchPriceCandidates(settings);
|
const candidates = await fetchPriceCandidates(settings);
|
||||||
if (!candidates.length) throw new Error('当前国家和价格范围内没有可用库存。');
|
if (!candidates.length) throw new Error('当前服务/价格范围内没有可用库存。');
|
||||||
const errors = [];
|
const errors = [];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
try {
|
try {
|
||||||
const activation = await requestActivation(settings, candidate);
|
const activation = await requestActivation(settings, candidate);
|
||||||
await saveSettings({ activeActivation: { ...activation, replacedReason: reason, previousActivationId: previous?.activationId || '' } });
|
await saveSettings({ activeActivation: { ...activation, replacedReason: reason, previousActivationId: previous?.activationId || '' } });
|
||||||
return { activation, releasePrevious, cancelPrevious: releasePrevious, tried: candidates.length, reason, releaseAction };
|
return { activation, releasePrevious, cancelPrevious: releasePrevious, tried: candidates.length, reason, releaseAction };
|
||||||
} catch (error) {
|
} catch (error) { errors.push(`${candidate.country.label}: ${error.message || error}`); }
|
||||||
errors.push(`${candidate.country.label}: ${error.message || error}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new Error(`获取手机号失败:${errors.join(' | ')}`);
|
throw new Error(`获取手机号失败:${errors.join(' | ')}`);
|
||||||
}
|
}
|
||||||
@@ -234,8 +265,20 @@ async function fetchActiveCode() {
|
|||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
const activation = settings.activeActivation;
|
const activation = settings.activeActivation;
|
||||||
if (!activation?.activationId) throw new Error('当前没有激活号码,请先获取手机号。');
|
if (!activation?.activationId) throw new Error('当前没有激活号码,请先获取手机号。');
|
||||||
const payload = await apiRequest(settings, { action: 'getStatus', id: activation.activationId }, '查询当前验证码');
|
const provider = getProvider(settings);
|
||||||
|
const payload = provider.id === 'luban-sms'
|
||||||
|
? await apiRequest(settings, { __endpoint: 'getSms', request_id: activation.activationId }, '查询 Luban 验证码')
|
||||||
|
: await apiRequest(settings, { action: 'getStatus', id: activation.activationId }, '查询当前验证码');
|
||||||
const message = describePayload(payload);
|
const message = describePayload(payload);
|
||||||
|
if (provider.id === 'luban-sms') {
|
||||||
|
if (payload?.msg === 'success' && payload.sms_code) {
|
||||||
|
const code = extractVerificationCode(payload.sms_code);
|
||||||
|
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
|
||||||
|
return { code, raw: message, activationId: activation.activationId, phoneNumber: activation.phoneNumber };
|
||||||
|
}
|
||||||
|
if (payload?.msg === 'wait') throw new Error('验证码还没到,稍后再点“复制验证码”。');
|
||||||
|
throw new Error(`暂未获取到验证码:${message || '空响应'}`);
|
||||||
|
}
|
||||||
if (/^STATUS_OK\s*:/i.test(message)) {
|
if (/^STATUS_OK\s*:/i.test(message)) {
|
||||||
const code = extractVerificationCode(message);
|
const code = extractVerificationCode(message);
|
||||||
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
|
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
|
||||||
@@ -246,23 +289,19 @@ async function fetchActiveCode() {
|
|||||||
throw new Error(`暂未获取到验证码:${message || '空响应'}`);
|
throw new Error(`暂未获取到验证码:${message || '空响应'}`);
|
||||||
}
|
}
|
||||||
async function fetchCountries(settings) {
|
async function fetchCountries(settings) {
|
||||||
|
if (getProvider(settings).id === 'luban-sms') return [{ id: -1, label: 'Luban service_id 模式' }];
|
||||||
try {
|
try {
|
||||||
const payload = await apiRequest(settings, { action: 'getCountries' }, '获取国家列表');
|
const payload = await apiRequest(settings, { action: 'getCountries' }, '获取国家列表');
|
||||||
const list = [];
|
const list = [];
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) payload.forEach((x) => list.push({ id: normalizeCountryId(x.id ?? x.country ?? x.code, -1), label: asText(x.name ?? x.label ?? x.title, '') }));
|
||||||
payload.forEach((x) => list.push({ id: normalizeCountryId(x.id ?? x.country ?? x.code, -1), label: asText(x.name ?? x.label ?? x.title, '') }));
|
else if (payload && typeof payload === 'object') Object.entries(payload).forEach(([id, value]) => {
|
||||||
} else if (payload && typeof payload === 'object') {
|
if (value && typeof value === 'object') list.push({ id: normalizeCountryId(value.id ?? id, -1), label: asText(value.name ?? value.label ?? value.title, `Country #${id}`) });
|
||||||
Object.entries(payload).forEach(([id, value]) => {
|
else list.push({ id: normalizeCountryId(id, -1), label: asText(value, `Country #${id}`) });
|
||||||
if (value && typeof value === 'object') list.push({ id: normalizeCountryId(value.id ?? id, -1), label: asText(value.name ?? value.label ?? value.title, `Country #${id}`) });
|
});
|
||||||
else list.push({ id: normalizeCountryId(id, -1), label: asText(value, `Country #${id}`) });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const normalized = list.filter((x) => x.id >= 0 && x.label && !seen.has(x.id) && seen.add(x.id)).sort((a, b) => a.id - b.id);
|
const normalized = list.filter((x) => x.id >= 0 && x.label && !seen.has(x.id) && seen.add(x.id)).sort((a, b) => a.id - b.id);
|
||||||
return normalized.length ? normalized : FALLBACK_COUNTRIES.slice();
|
return normalized.length ? normalized : FALLBACK_COUNTRIES.slice();
|
||||||
} catch {
|
} catch { return FALLBACK_COUNTRIES.slice(); }
|
||||||
return FALLBACK_COUNTRIES.slice();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.action.onClicked.addListener(async (tab) => {
|
chrome.action.onClicked.addListener(async (tab) => {
|
||||||
@@ -270,9 +309,7 @@ chrome.action.onClicked.addListener(async (tab) => {
|
|||||||
try {
|
try {
|
||||||
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
|
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
|
||||||
await chrome.tabs.sendMessage(tab.id, { type: 'TOGGLE_FLOAT_PANEL' });
|
await chrome.tabs.sendMessage(tab.id, { type: 'TOGGLE_FLOAT_PANEL' });
|
||||||
} catch (error) {
|
} catch (error) { console.warn('SMSBower 悬浮窗打开失败:', error); }
|
||||||
console.warn('SMSBower 悬浮窗打开失败:', error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
|||||||
+24
-6
@@ -177,9 +177,12 @@
|
|||||||
|
|
||||||
function collectSettings() {
|
function collectSettings() {
|
||||||
return {
|
return {
|
||||||
|
provider: $('.sb-provider')?.value || 'sms-bower',
|
||||||
apiKey: $('.sb-api-key')?.value.trim() || '',
|
apiKey: $('.sb-api-key')?.value.trim() || '',
|
||||||
baseUrl: $('.sb-base-url')?.value.trim() || '',
|
baseUrl: $('.sb-base-url')?.value.trim() || '',
|
||||||
|
lubanBaseUrl: $('.sb-luban-base-url')?.value.trim() || '',
|
||||||
serviceCode: $('.sb-service-code')?.value.trim() || 'dr',
|
serviceCode: $('.sb-service-code')?.value.trim() || 'dr',
|
||||||
|
lubanServiceId: $('.sb-luban-service-id')?.value.trim() || '',
|
||||||
minPrice: $('.sb-min-price')?.value.trim() || '',
|
minPrice: $('.sb-min-price')?.value.trim() || '',
|
||||||
maxPrice: $('.sb-max-price')?.value.trim() || '',
|
maxPrice: $('.sb-max-price')?.value.trim() || '',
|
||||||
countries: selectedCountries,
|
countries: selectedCountries,
|
||||||
@@ -246,6 +249,13 @@
|
|||||||
btn.classList.toggle('sb-auto-on', autoRotateEnabled);
|
btn.classList.toggle('sb-auto-on', autoRotateEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateProviderFields() {
|
||||||
|
const provider = $('.sb-provider')?.value || 'sms-bower';
|
||||||
|
const isLuban = provider === 'luban-sms';
|
||||||
|
panel?.querySelectorAll('.sb-smsbower-only').forEach((el) => { el.style.display = isLuban ? 'none' : ''; });
|
||||||
|
panel?.querySelectorAll('.sb-luban-only').forEach((el) => { el.style.display = isLuban ? '' : 'none'; });
|
||||||
|
}
|
||||||
|
|
||||||
function getCurrentPhoneNumber() {
|
function getCurrentPhoneNumber() {
|
||||||
const phone = $('.sb-current-phone')?.textContent || latestSettings?.activeActivation?.phoneNumber || '';
|
const phone = $('.sb-current-phone')?.textContent || latestSettings?.activeActivation?.phoneNumber || '';
|
||||||
return String(phone).trim();
|
return String(phone).trim();
|
||||||
@@ -416,7 +426,7 @@
|
|||||||
#${PANEL_ID} .sb-title { font-size: 16px; font-weight: 700; }
|
#${PANEL_ID} .sb-title { font-size: 16px; font-weight: 700; }
|
||||||
#${PANEL_ID} .sb-close { width: 28px; height: 28px; padding: 0; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #1f2937; cursor: pointer; }
|
#${PANEL_ID} .sb-close { width: 28px; height: 28px; padding: 0; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #1f2937; cursor: pointer; }
|
||||||
#${PANEL_ID} label { display: grid; gap: 4px; margin: 7px 0; color: #cbd5e1; }
|
#${PANEL_ID} label { display: grid; gap: 4px; margin: 7px 0; color: #cbd5e1; }
|
||||||
#${PANEL_ID} input { width: 100%; padding: 7px 9px; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #111827; outline: none; }
|
#${PANEL_ID} input, #${PANEL_ID} select { width: 100%; padding: 7px 9px; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #111827; outline: none; }
|
||||||
#${PANEL_ID} input:focus { border-color: #38bdf8; box-shadow: 0 0 0 2px rgba(56,189,248,.18); }
|
#${PANEL_ID} input:focus { border-color: #38bdf8; box-shadow: 0 0 0 2px rgba(56,189,248,.18); }
|
||||||
#${PANEL_ID} .sb-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
#${PANEL_ID} .sb-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||||||
#${PANEL_ID} .sb-country-box { margin-top: 9px; padding: 9px; border: 1px solid #253244; border-radius: 10px; background: #111827; }
|
#${PANEL_ID} .sb-country-box { margin-top: 9px; padding: 9px; border: 1px solid #253244; border-radius: 10px; background: #111827; }
|
||||||
@@ -450,19 +460,22 @@
|
|||||||
panel.id = PANEL_ID;
|
panel.id = PANEL_ID;
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div class="sb-head">
|
<div class="sb-head">
|
||||||
<span class="sb-title">SMSBower 填号</span>
|
<span class="sb-title">接码填号</span>
|
||||||
<button class="sb-close" type="button" title="隐藏">×</button>
|
<button class="sb-close" type="button" title="隐藏">×</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="sb-fetch sb-primary sb-top-fetch" type="button">获取并填入</button>
|
<button class="sb-fetch sb-primary sb-top-fetch" type="button">获取并填入</button>
|
||||||
<button class="sb-auto-toggle sb-auto-on" type="button">自动切号:开</button>
|
<button class="sb-auto-toggle sb-auto-on" type="button">自动切号:开</button>
|
||||||
<label>API Key <input class="sb-api-key" type="password" placeholder="SMSBower API Key" /></label>
|
<label>接码平台 <select class="sb-provider"><option value="sms-bower">SMSBower</option><option value="luban-sms">Luban SMS</option></select></label>
|
||||||
<label>API 地址 <input class="sb-base-url" type="url" /></label>
|
<label>API Key <input class="sb-api-key" type="password" placeholder="API Key / apikey" /></label>
|
||||||
|
<label class="sb-smsbower-only">SMSBower API 地址 <input class="sb-base-url" type="url" /></label>
|
||||||
|
<label class="sb-luban-only">Luban API 地址 <input class="sb-luban-base-url" type="url" /></label>
|
||||||
<div class="sb-row">
|
<div class="sb-row">
|
||||||
<label>项目 <input class="sb-service-code" type="text" value="dr" /></label>
|
<label class="sb-smsbower-only">SMSBower 项目 <input class="sb-service-code" type="text" value="dr" /></label>
|
||||||
|
<label class="sb-luban-only">Luban service_id <input class="sb-luban-service-id" type="text" placeholder="如 121949" /></label>
|
||||||
<label>最低价 <input class="sb-min-price" type="number" min="0" step="0.0001" placeholder="可空" /></label>
|
<label>最低价 <input class="sb-min-price" type="number" min="0" step="0.0001" placeholder="可空" /></label>
|
||||||
<label>最高价 <input class="sb-max-price" type="number" min="0" step="0.0001" placeholder="可空" /></label>
|
<label>最高价 <input class="sb-max-price" type="number" min="0" step="0.0001" placeholder="可空" /></label>
|
||||||
</div>
|
</div>
|
||||||
<section class="sb-country-box">
|
<section class="sb-country-box sb-smsbower-only">
|
||||||
<div class="sb-country-head"><strong>国家顺序</strong><button class="sb-refresh" type="button">刷新国家</button></div>
|
<div class="sb-country-head"><strong>国家顺序</strong><button class="sb-refresh" type="button">刷新国家</button></div>
|
||||||
<input class="sb-country-search" type="search" placeholder="搜索国家 / ID" />
|
<input class="sb-country-search" type="search" placeholder="搜索国家 / ID" />
|
||||||
<div class="sb-selected-countries"></div>
|
<div class="sb-selected-countries"></div>
|
||||||
@@ -497,6 +510,7 @@
|
|||||||
function bindPanelEvents() {
|
function bindPanelEvents() {
|
||||||
$('.sb-close').addEventListener('click', () => { panel.style.display = 'none'; });
|
$('.sb-close').addEventListener('click', () => { panel.style.display = 'none'; });
|
||||||
$('.sb-save').addEventListener('click', saveSettings);
|
$('.sb-save').addEventListener('click', saveSettings);
|
||||||
|
$('.sb-provider').addEventListener('change', updateProviderFields);
|
||||||
$('.sb-fetch').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' }));
|
$('.sb-fetch').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' }));
|
||||||
$('.sb-auto-toggle').addEventListener('click', () => {
|
$('.sb-auto-toggle').addEventListener('click', () => {
|
||||||
autoRotateEnabled = !autoRotateEnabled;
|
autoRotateEnabled = !autoRotateEnabled;
|
||||||
@@ -540,9 +554,12 @@
|
|||||||
latestSettings = res.settings;
|
latestSettings = res.settings;
|
||||||
allCountries = (res.fallbackCountries || FALLBACK_COUNTRIES).map(normalizeCountry);
|
allCountries = (res.fallbackCountries || FALLBACK_COUNTRIES).map(normalizeCountry);
|
||||||
selectedCountries = (latestSettings.countries || []).map(normalizeCountry);
|
selectedCountries = (latestSettings.countries || []).map(normalizeCountry);
|
||||||
|
$('.sb-provider').value = latestSettings.provider || 'sms-bower';
|
||||||
$('.sb-api-key').value = latestSettings.apiKey || '';
|
$('.sb-api-key').value = latestSettings.apiKey || '';
|
||||||
$('.sb-base-url').value = latestSettings.baseUrl || '';
|
$('.sb-base-url').value = latestSettings.baseUrl || '';
|
||||||
|
$('.sb-luban-base-url').value = latestSettings.lubanBaseUrl || 'https://lubansms.com/v2/api';
|
||||||
$('.sb-service-code').value = latestSettings.serviceCode || 'dr';
|
$('.sb-service-code').value = latestSettings.serviceCode || 'dr';
|
||||||
|
$('.sb-luban-service-id').value = latestSettings.lubanServiceId || '';
|
||||||
$('.sb-min-price').value = latestSettings.minPrice || '';
|
$('.sb-min-price').value = latestSettings.minPrice || '';
|
||||||
$('.sb-max-price').value = latestSettings.maxPrice || '';
|
$('.sb-max-price').value = latestSettings.maxPrice || '';
|
||||||
if (latestSettings.activeActivation) {
|
if (latestSettings.activeActivation) {
|
||||||
@@ -551,6 +568,7 @@
|
|||||||
const codeNode = $('.sb-current-code');
|
const codeNode = $('.sb-current-code');
|
||||||
if (codeNode) codeNode.textContent = latestSettings.activeActivation.lastCode || '无';
|
if (codeNode) codeNode.textContent = latestSettings.activeActivation.lastCode || '无';
|
||||||
}
|
}
|
||||||
|
updateProviderFields();
|
||||||
renderCountries();
|
renderCountries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "SMSBower Phone Fill Tool",
|
"name": "SMSBower Phone Fill Tool",
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"description": "Fetch one SMSBower phone number, fill it into the current page input, copy it, and cancel the previous activation on replacement.",
|
"description": "Fetch SMSBower or Luban SMS phone numbers, fill them into the current page, copy, auto-submit, rotate rejected numbers, and fetch codes.",
|
||||||
"permissions": ["activeTab", "scripting", "storage", "clipboardWrite"],
|
"permissions": ["activeTab", "scripting", "storage", "clipboardWrite"],
|
||||||
"host_permissions": ["https://smsbower.app/*", "https://smsbower.page/*", "https://smsbower.online/*"],
|
"host_permissions": ["https://smsbower.app/*", "https://smsbower.page/*", "https://smsbower.online/*", "https://lubansms.com/*"],
|
||||||
"background": { "service_worker": "background.js" },
|
"background": { "service_worker": "background.js" },
|
||||||
"action": { "default_title": "SMSBower 悬浮填号" }
|
"action": { "default_title": "SMSBower 悬浮填号" }
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-1
@@ -25,7 +25,7 @@ function load(extra = {}) {
|
|||||||
Set,
|
Set,
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
const fn = new Function('globalThis', 'self', `with (globalThis) { ${source}; return { collectPriceEntries, filterPriceEntries, normalizeActivation, extractVerificationCode, isPhoneNumberUsedError, isPhoneNumberDeliveryRefusedError, getPhoneReplacementReleaseAction, fetchNextPhone, normalizeSettings, buildUrl }; }`);
|
const fn = new Function('globalThis', 'self', `with (globalThis) { ${source}; return { collectPriceEntries, filterPriceEntries, normalizeActivation, extractVerificationCode, isPhoneNumberUsedError, isPhoneNumberDeliveryRefusedError, getPhoneReplacementReleaseAction, fetchNextPhone, normalizeSettings, buildUrl, fetchActiveCode }; }`);
|
||||||
return fn(sandbox, sandbox);
|
return fn(sandbox, sandbox);
|
||||||
}
|
}
|
||||||
test('collectPriceEntries parses keyed price inventory', () => {
|
test('collectPriceEntries parses keyed price inventory', () => {
|
||||||
@@ -112,3 +112,93 @@ test('fetchNextPhone releases previous activation then acquires replacement', as
|
|||||||
['getNumberV2', null, null, '6'],
|
['getNumberV2', null, null, '6'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('buildUrl uses Luban apikey parameter and base endpoint', () => {
|
||||||
|
const api = load();
|
||||||
|
const url = new URL(api.buildUrl(
|
||||||
|
{ provider: 'luban-sms', lubanBaseUrl: 'https://lubansms.com/v2/api/getNumber', apiKey: 'luban-key' },
|
||||||
|
{ service_id: '121949' }
|
||||||
|
));
|
||||||
|
assert.equal(url.searchParams.get('apikey'), 'luban-key');
|
||||||
|
assert.equal(url.searchParams.get('api_key'), null);
|
||||||
|
assert.equal(url.searchParams.get('service_id'), '121949');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeActivation parses Luban getNumber response', () => {
|
||||||
|
const api = load();
|
||||||
|
const activation = api.normalizeActivation(
|
||||||
|
{ code: 0, msg: '', number: '79781901206', request_id: 230698 },
|
||||||
|
{ provider: 'luban-sms', serviceCode: '121949' }
|
||||||
|
);
|
||||||
|
assert.equal(activation.provider, 'luban-sms');
|
||||||
|
assert.equal(activation.activationId, '230698');
|
||||||
|
assert.equal(activation.phoneNumber, '79781901206');
|
||||||
|
assert.equal(activation.serviceCode, '121949');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchNextPhone acquires Luban number and rejects previous request', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const stored = {
|
||||||
|
provider: 'luban-sms',
|
||||||
|
apiKey: 'luban-key',
|
||||||
|
lubanBaseUrl: 'https://lubansms.com/v2/api/getNumber',
|
||||||
|
lubanServiceId: '121949',
|
||||||
|
activeActivation: { activationId: 'old-request', phoneNumber: '79780000000', provider: 'luban-sms' },
|
||||||
|
};
|
||||||
|
const api = load({
|
||||||
|
chrome: {
|
||||||
|
action: { onClicked: { addListener() {} } },
|
||||||
|
tabs: { async sendMessage() {} },
|
||||||
|
scripting: { async executeScript() {} },
|
||||||
|
runtime: { onMessage: { addListener() {} } },
|
||||||
|
storage: { local: { async get() { return stored; }, async set(update) { Object.assign(stored, update); } } }
|
||||||
|
},
|
||||||
|
fetch: async (url) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
requests.push([parsed.pathname, parsed.searchParams.get('apikey'), parsed.searchParams.get('request_id'), parsed.searchParams.get('status'), parsed.searchParams.get('service_id')]);
|
||||||
|
if (parsed.pathname.endsWith('/setStatus')) return new Response(JSON.stringify({ code: 0, msg: 'success' }), { status: 200 });
|
||||||
|
if (parsed.pathname.endsWith('/List')) return new Response(JSON.stringify({ code: 0, msg: [] }), { status: 200 });
|
||||||
|
if (parsed.pathname.endsWith('/getNumber')) return new Response(JSON.stringify({ code: 0, msg: '', number: '79781901206', request_id: 230698 }), { status: 200 });
|
||||||
|
throw new Error(`unexpected ${url}`);
|
||||||
|
},
|
||||||
|
Response,
|
||||||
|
});
|
||||||
|
const result = await api.fetchNextPhone({ reason: 'manual_replace', releaseAction: 'cancel' });
|
||||||
|
assert.equal(result.activation.provider, 'luban-sms');
|
||||||
|
assert.equal(result.activation.activationId, '230698');
|
||||||
|
assert.equal(stored.activeActivation.previousActivationId, 'old-request');
|
||||||
|
assert.deepEqual(requests, [
|
||||||
|
['/v2/api/setStatus', 'luban-key', 'old-request', 'reject', null],
|
||||||
|
['/v2/api/List', 'luban-key', null, null, null],
|
||||||
|
['/v2/api/getNumber', 'luban-key', null, null, '121949'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchActiveCode parses Luban success SMS code', async () => {
|
||||||
|
const stored = {
|
||||||
|
provider: 'luban-sms',
|
||||||
|
apiKey: 'luban-key',
|
||||||
|
lubanBaseUrl: 'https://lubansms.com/v2/api/getSms',
|
||||||
|
activeActivation: { activationId: '230698', phoneNumber: '79781901206', provider: 'luban-sms' },
|
||||||
|
};
|
||||||
|
const api = load({
|
||||||
|
chrome: {
|
||||||
|
action: { onClicked: { addListener() {} } },
|
||||||
|
tabs: { async sendMessage() {} },
|
||||||
|
scripting: { async executeScript() {} },
|
||||||
|
runtime: { onMessage: { addListener() {} } },
|
||||||
|
storage: { local: { async get() { return stored; }, async set(update) { Object.assign(stored, update); } } }
|
||||||
|
},
|
||||||
|
fetch: async (url) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
assert.equal(parsed.pathname, '/v2/api/getSms');
|
||||||
|
assert.equal(parsed.searchParams.get('request_id'), '230698');
|
||||||
|
return new Response(JSON.stringify({ code: 0, msg: 'success', sms_code: '380682' }), { status: 200 });
|
||||||
|
},
|
||||||
|
Response,
|
||||||
|
});
|
||||||
|
const result = await api.fetchActiveCode();
|
||||||
|
assert.equal(result.code, '380682');
|
||||||
|
assert.equal(stored.activeActivation.lastCode, '380682');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user