From 09ccc76e71ce3bce41ea20ae59ea5904a613a945 Mon Sep 17 00:00:00 2001 From: chick Date: Sun, 31 May 2026 19:13:45 +0800 Subject: [PATCH] feat: add Luban SMS provider --- README.md | 28 ++++--- background.js | 173 +++++++++++++++++++++++++---------------- content.js | 30 +++++-- manifest.json | 6 +- tests/provider.test.js | 92 +++++++++++++++++++++- 5 files changed, 239 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index b982060..880ba7e 100644 --- a/README.md +++ b/README.md @@ -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。 @@ -17,22 +20,23 @@ SMSBower Phone Fill Tool 使用: 1. 打开任意网页,先点一下你要填写的输入框。 -2. 点浏览器工具栏里的“SMSBower 悬浮填号”扩展图标。 +2. 点浏览器工具栏里的“接码填号”扩展图标。 3. 页面右上角会出现悬浮窗,可拖动。 -4. 填 SMSBower API Key、项目代码、价格上下限、国家顺序。 +4. 选择接码平台: + - SMSBower:填写 API Key、项目代码、价格上下限、国家顺序。 + - Luban SMS:填写 API Key 和 `service_id`;`service_id` 可在 Luban 控制台或 API 服务列表中查询。Luban 的国家/服务由 `service_id` 决定,所以国家选择会隐藏。 5. 顶部“获取并填入”会立即拿号、填入、复制,并自动提交/点击发送验证码。 6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。 7. 如果目标页面拒绝号码,保持悬浮窗打开即可:插件会自动扫描页面错误文本并切换下一个号码。 - “自动切号:开/关”可以临时启停自动扫描; - “检测报错换号”用于手动触发一次扫描; - “已使用换号”用于你已经确认号码被使用时立即换号。 -8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;如果 SMSBower 已收到 STATUS_OK,会显示验证码并停止。点获取新号、保存、刷新国家或取消当前会停止这轮验证码轮询。 +8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;收到验证码后显示并停止。 9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。 -10. 国家搜索支持中文别名,例如搜“印度”会出现 India 和 Indonesia。 -11. 点悬浮窗右上角 × 只是隐藏;再次点扩展图标会重新显示。 +10. 点悬浮窗右上角 × 只是隐藏;再次点扩展图标会重新显示。 说明: -- “复制验证码”只查询当前 SMSBower 激活订单的 getStatus,不会自动填验证码。 -- 拿号优先级:价格低优先;同价格按你选择国家的顺序优先。 -- SMSBower 默认接口地址为 https://smsbower.app/stubs/handler_api.php,可在悬浮窗里改。 +- “复制验证码”只复制当前已查询到的验证码,不会自动填验证码。 +- SMSBower 拿号优先级:价格低优先;同价格按你选择国家的顺序优先。 +- Luban SMS 官方文档基础地址为 `https://lubansms.com/v2/api/`,扩展会按接口自动拼接 `getNumber/getSms/setStatus/List`。 - 本地验证:`node --test tests/provider.test.js`。 diff --git a/background.js b/background.js index 5b29671..5569e53 100644 --- a/background.js +++ b/background.js @@ -1,7 +1,10 @@ const DEFAULT_SETTINGS = Object.freeze({ + provider: 'sms-bower', apiKey: '', baseUrl: 'https://smsbower.app/stubs/handler_api.php', + lubanBaseUrl: 'https://lubansms.com/v2/api', serviceCode: 'dr', + lubanServiceId: '', minPrice: '', maxPrice: '', countries: [{ id: 6, label: 'Indonesia' }], @@ -18,7 +21,18 @@ const FALLBACK_COUNTRIES = Object.freeze([ { 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 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) { const parsed = Math.floor(Number(value)); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; @@ -43,21 +57,23 @@ function isPhoneNumberDeliveryRefusedError(value) { } function getPhoneReplacementReleaseAction(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)) { - return 'ban'; - } + if (normalized === 'code_rejected' || normalized === 'phone_number_used' || normalized === 'phone_number_in_use' || normalized === 'phone_max_usage_exceeded' || isPhoneNumberUsedError(reason)) return 'ban'; return 'cancel'; } function normalizeSettings(input = {}) { 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)}`) })) : DEFAULT_SETTINGS.countries; + const provider = normalizeProvider(input.provider); return { ...DEFAULT_SETTINGS, ...input, + provider, apiKey: asText(input.apiKey), 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, + 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)), maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)), countries, @@ -73,20 +89,31 @@ async function saveSettings(update = {}) { await chrome.storage.local.set(next); return next; } +function providerBaseUrl(settings) { + const provider = getProvider(settings); + return asText(settings[provider.baseUrlKey], provider.defaultBaseUrl); +} function buildUrl(settings, query = {}) { - const url = new URL(settings.baseUrl || DEFAULT_SETTINGS.baseUrl); - if (settings.apiKey) url.searchParams.set('api_key', settings.apiKey); - Object.entries(query).forEach(([k, v]) => { - if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v)); - }); + const provider = getProvider(settings); + let base = providerBaseUrl(settings); + const queryEntries = { ...query }; + 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(); } function parsePayload(text) { const t = String(text || '').trim(); if (!t) return ''; - if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) { - try { return JSON.parse(t); } catch {} - } + if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) { try { return JSON.parse(t); } catch {} } return t; } function describePayload(payload) { @@ -98,13 +125,15 @@ function describePayload(payload) { } return String(payload || '').trim(); } -async function apiRequest(settings, query, label = 'SMSBower 请求') { - if (!settings.apiKey) throw new Error('请先填写 SMSBower API Key。'); +async function apiRequest(settings, query, label = '接码平台请求') { + 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 payload = parsePayload(await response.text()); const message = describePayload(payload); 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}`); } return payload; @@ -115,18 +144,16 @@ function normalizeActivation(payload, fallback = {}) { if (m) payload = { activationId: m[1], phoneNumber: m[2] }; } 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); if (!activationId || !phoneNumber) return null; const countryId = normalizeCountryId(src.countryId ?? src.country_id ?? src.country ?? fallback.countryId, fallback.countryId ?? 6); return { - activationId, - phoneNumber, - provider: 'sms-bower', - serviceCode: asText(src.serviceCode ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode), + activationId, phoneNumber, provider: fallback.provider || 'sms-bower', + serviceCode: asText(src.serviceCode ?? src.service_id ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode), countryId, - countryLabel: asText(src.countryLabel ?? fallback.countryLabel, fallback.countryLabel || `Country #${countryId}`), - price: Number.isFinite(Number(src.price ?? fallback.price)) ? Number(src.price ?? fallback.price) : null, + countryLabel: asText(src.countryLabel ?? src.country_name_zh ?? src.country_name_en ?? fallback.countryLabel, fallback.countryLabel || `Country #${countryId}`), + price: Number.isFinite(Number(src.price ?? src.cost ?? fallback.price)) ? Number(src.price ?? src.cost ?? fallback.price) : null, acquiredAt: Date.now(), }; } @@ -154,7 +181,7 @@ function filterPriceEntries(entries, settings) { 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)); } -async function fetchPriceCandidates(settings) { +async function fetchSmsBowerCandidates(settings) { const out = []; for (let index = 0; index < settings.countries.length; index += 1) { 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 prices = filterPriceEntries(collectPriceEntries(payload), settings); prices.forEach((entry) => out.push({ country, countryIndex: index, price: entry.cost, count: entry.count })); - } catch (error) { - out.push({ country, countryIndex: index, price: Number.POSITIVE_INFINITY, count: 0, error: error.message || String(error) }); - } + } catch (error) { out.push({ country, countryIndex: index, price: Number.POSITIVE_INFINITY, count: 0, error: error.message || String(error) }); } } - return out - .filter((x) => Number.isFinite(x.price)) - .sort((a, b) => (a.price - b.price) || (a.countryIndex - b.countryIndex)); + return out.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) { if (!activation?.activationId) return { ok: false, skipped: true, releaseAction }; + const provider = getProvider(settings); const normalizedAction = String(releaseAction || '').trim().toLowerCase() === 'ban' ? 'ban' : 'cancel'; try { - // SMSBower uses the same lifecycle endpoint/status for cancel-style release here. - const payload = await apiRequest(settings, { action: 'setStatus', status: 8, id: activation.activationId }, normalizedAction === 'ban' ? '释放被拒号码' : '取消上一个号码激活'); + let payload; + 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 }; } catch (error) { if (!quiet) throw error; return { ok: false, error: error.message || String(error), releaseAction: normalizedAction, activationId: activation.activationId }; } } -async function cancelActivation(settings, activation, quiet = false) { - return releaseActivation(settings, activation, 'cancel', quiet); -} -async function banActivation(settings, activation, quiet = false) { - return releaseActivation(settings, activation, 'ban', quiet); -} +async function cancelActivation(settings, activation, quiet = false) { return releaseActivation(settings, activation, 'cancel', quiet); } +async function banActivation(settings, activation, quiet = false) { return releaseActivation(settings, activation, 'ban', quiet); } async function requestActivation(settings, candidate) { - 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}`); - const activation = normalizeActivation(payload, { - countryId: candidate.country.id, - countryLabel: candidate.country.label, - serviceCode: settings.serviceCode, - price: candidate.price, - }); + const provider = getProvider(settings); + if (provider.id === 'luban-sms') { + const payload = await apiRequest(settings, { __endpoint: 'getNumber', service_id: candidate.serviceId || settings.lubanServiceId }, '购买 Luban 手机号'); + const activation = normalizeActivation(payload, { provider: provider.id, serviceCode: settings.lubanServiceId, price: candidate.price, countryId: -1, countryLabel: 'Luban service' }); + if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`); + return activation; + } + 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}`); + const activation = normalizeActivation(payload, { provider: provider.id, countryId: candidate.country.id, countryLabel: candidate.country.label, serviceCode: settings.serviceCode, price: candidate.price }); if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`); return activation; } @@ -212,16 +245,14 @@ async function fetchNextPhone(options = {}) { const releaseAction = options.releaseAction || getPhoneReplacementReleaseAction(reason); const releasePrevious = previous ? await releaseActivation(settings, previous, releaseAction, true) : { ok: false, skipped: true, releaseAction }; const candidates = await fetchPriceCandidates(settings); - if (!candidates.length) throw new Error('当前国家和价格范围内没有可用库存。'); + if (!candidates.length) throw new Error('当前服务/价格范围内没有可用库存。'); const errors = []; for (const candidate of candidates) { try { const activation = await requestActivation(settings, candidate); await saveSettings({ activeActivation: { ...activation, replacedReason: reason, previousActivationId: previous?.activationId || '' } }); return { activation, releasePrevious, cancelPrevious: releasePrevious, tried: candidates.length, reason, releaseAction }; - } catch (error) { - errors.push(`${candidate.country.label}: ${error.message || error}`); - } + } catch (error) { errors.push(`${candidate.country.label}: ${error.message || error}`); } } throw new Error(`获取手机号失败:${errors.join(' | ')}`); } @@ -234,8 +265,20 @@ async function fetchActiveCode() { const settings = await getSettings(); const activation = settings.activeActivation; 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); + 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)) { const code = extractVerificationCode(message); await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } }); @@ -246,23 +289,19 @@ async function fetchActiveCode() { throw new Error(`暂未获取到验证码:${message || '空响应'}`); } async function fetchCountries(settings) { + if (getProvider(settings).id === 'luban-sms') return [{ id: -1, label: 'Luban service_id 模式' }]; try { const payload = await apiRequest(settings, { action: 'getCountries' }, '获取国家列表'); const list = []; - 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, '') })); - } else if (payload && typeof payload === 'object') { - Object.entries(payload).forEach(([id, value]) => { - 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}`) }); - }); - } + 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, '') })); + else if (payload && typeof payload === 'object') Object.entries(payload).forEach(([id, value]) => { + 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 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(); - } catch { - return FALLBACK_COUNTRIES.slice(); - } + } catch { return FALLBACK_COUNTRIES.slice(); } } chrome.action.onClicked.addListener(async (tab) => { @@ -270,9 +309,7 @@ chrome.action.onClicked.addListener(async (tab) => { try { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] }); await chrome.tabs.sendMessage(tab.id, { type: 'TOGGLE_FLOAT_PANEL' }); - } catch (error) { - console.warn('SMSBower 悬浮窗打开失败:', error); - } + } catch (error) { console.warn('SMSBower 悬浮窗打开失败:', error); } }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { diff --git a/content.js b/content.js index 3794caa..395815e 100644 --- a/content.js +++ b/content.js @@ -177,9 +177,12 @@ function collectSettings() { return { + provider: $('.sb-provider')?.value || 'sms-bower', apiKey: $('.sb-api-key')?.value.trim() || '', baseUrl: $('.sb-base-url')?.value.trim() || '', + lubanBaseUrl: $('.sb-luban-base-url')?.value.trim() || '', serviceCode: $('.sb-service-code')?.value.trim() || 'dr', + lubanServiceId: $('.sb-luban-service-id')?.value.trim() || '', minPrice: $('.sb-min-price')?.value.trim() || '', maxPrice: $('.sb-max-price')?.value.trim() || '', countries: selectedCountries, @@ -246,6 +249,13 @@ 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() { const phone = $('.sb-current-phone')?.textContent || latestSettings?.activeActivation?.phoneNumber || ''; return String(phone).trim(); @@ -416,7 +426,7 @@ #${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} 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} .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; } @@ -450,19 +460,22 @@ panel.id = PANEL_ID; panel.innerHTML = `
- SMSBower 填号 + 接码填号
- - + + + +
- + +
-
+
国家顺序
@@ -497,6 +510,7 @@ function bindPanelEvents() { $('.sb-close').addEventListener('click', () => { panel.style.display = 'none'; }); $('.sb-save').addEventListener('click', saveSettings); + $('.sb-provider').addEventListener('change', updateProviderFields); $('.sb-fetch').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' })); $('.sb-auto-toggle').addEventListener('click', () => { autoRotateEnabled = !autoRotateEnabled; @@ -540,9 +554,12 @@ latestSettings = res.settings; allCountries = (res.fallbackCountries || FALLBACK_COUNTRIES).map(normalizeCountry); selectedCountries = (latestSettings.countries || []).map(normalizeCountry); + $('.sb-provider').value = latestSettings.provider || 'sms-bower'; $('.sb-api-key').value = latestSettings.apiKey || ''; $('.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-luban-service-id').value = latestSettings.lubanServiceId || ''; $('.sb-min-price').value = latestSettings.minPrice || ''; $('.sb-max-price').value = latestSettings.maxPrice || ''; if (latestSettings.activeActivation) { @@ -551,6 +568,7 @@ const codeNode = $('.sb-current-code'); if (codeNode) codeNode.textContent = latestSettings.activeActivation.lastCode || '无'; } + updateProviderFields(); renderCountries(); } diff --git a/manifest.json b/manifest.json index c934cd6..a27b2c5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 3, "name": "SMSBower Phone Fill Tool", - "version": "1.4.0", - "description": "Fetch one SMSBower phone number, fill it into the current page input, copy it, and cancel the previous activation on replacement.", + "version": "1.5.0", + "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"], - "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" }, "action": { "default_title": "SMSBower 悬浮填号" } } diff --git a/tests/provider.test.js b/tests/provider.test.js index 4617103..780b2bf 100644 --- a/tests/provider.test.js +++ b/tests/provider.test.js @@ -25,7 +25,7 @@ function load(extra = {}) { Set, ...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); } test('collectPriceEntries parses keyed price inventory', () => { @@ -112,3 +112,93 @@ test('fetchNextPhone releases previous activation then acquires replacement', as ['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'); +});