feat: add smsbower phone fill tool with rotation

This commit is contained in:
chick
2026-05-31 16:05:42 +08:00
commit a9c7cf4ae1
8 changed files with 1120 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
SMSBower Phone Fill Tool
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。点一次“获取并填入”就获取一个 SMSBower 手机号,自动填入当前页面最像手机号的输入框,并复制到剪贴板。再次获取会先释放上一个激活,再拿新号替换页面输入框。
本版已集成 FlowPilot 同款切号机制:
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,可点“检测报错换号”自动识别,或直接点“已使用换号”;旧号按被拒原因释放,新号自动填入并复制。
- 页面提示“无法向此电话号码发送验证码”等发送失败时,“检测报错换号”会按可恢复失败换号。
- 仍然只负责填号/查码/复制,不自动点击目标页面继续按钮。
安装/更新:
1. 打开 Chrome/Edge 扩展管理页。
2. 开启“开发者模式”。
3. 加载已解压的扩展,选择本目录。
4. 如果之前已经加载过,点扩展卡片上的“重新加载”。
使用:
1. 打开任意网页,先点一下你要填写的输入框。
2. 点浏览器工具栏里的“SMSBower 悬浮填号”扩展图标。
3. 页面右上角会出现悬浮窗,可拖动。
4. 填 SMSBower API Key、项目代码、价格上下限、国家顺序。
5. 顶部“获取并填入”会立即拿号并填入。
6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。
7. 如果目标页面拒绝号码:
- 先点“检测报错换号”,插件会扫描当前页面文本,识别“已使用/发送验证码失败”等错误并换号;
- 如果你已经确认是“号码已使用”,可直接点“已使用换号”。
8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;如果 SMSBower 已收到 STATUS_OK,会显示验证码并停止。点获取新号、保存、刷新国家或取消当前会停止这轮验证码轮询。
9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。
10. 国家搜索支持中文别名,例如搜“印度”会出现 India 和 Indonesia。
11. 点悬浮窗右上角 × 只是隐藏;再次点扩展图标会重新显示。
说明:
- “复制验证码”只查询当前 SMSBower 激活订单的 getStatus,不会自动填验证码。
- 拿号优先级:价格低优先;同价格按你选择国家的顺序优先。
- SMSBower 默认接口地址为 https://smsbower.app/stubs/handler_api.php,可在悬浮窗里改。
- 本地验证:`node --test tests/provider.test.js`
+301
View File
@@ -0,0 +1,301 @@
const DEFAULT_SETTINGS = Object.freeze({
apiKey: '',
baseUrl: 'https://smsbower.app/stubs/handler_api.php',
serviceCode: 'dr',
minPrice: '',
maxPrice: '',
countries: [{ id: 6, label: 'Indonesia' }],
activeActivation: null,
});
const FALLBACK_COUNTRIES = Object.freeze([
{ id: 0, label: 'Russia' }, { id: 1, label: 'Ukraine' }, { id: 2, label: 'Kazakhstan' },
{ id: 3, label: 'China' }, { id: 4, label: 'Philippines' }, { id: 5, label: 'Myanmar' },
{ id: 6, label: 'Indonesia' }, { id: 7, label: 'Malaysia' }, { id: 10, label: 'Vietnam' },
{ id: 12, label: 'USA virtual' }, { id: 16, label: 'United Kingdom' }, { id: 22, label: 'India' },
{ id: 32, label: 'Romania' }, { id: 36, label: 'Canada' }, { id: 43, label: 'Germany' },
{ id: 52, label: 'Thailand' }, { id: 73, label: 'France' }, { id: 78, label: 'Turkey' },
{ id: 151, label: 'Japan' }, { id: 187, label: 'USA' }
]);
function asText(value = '', fallback = '') { return String(value ?? '').trim() || fallback; }
function normalizeCountryId(value, fallback = 6) {
const parsed = Math.floor(Number(value));
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}
function normalizePrice(value) {
if (value === undefined || value === null || String(value).trim() === '') return null;
const n = Number(value);
return Number.isFinite(n) && n > 0 ? Math.round(n * 10000) / 10000 : null;
}
function normalizeFailureReason(value = '') {
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '') || 'manual_replace';
}
function isPhoneNumberUsedError(value) {
const text = String(value || '').trim();
if (!text) return false;
return /phone_max_usage_exceeded|phone_number_in_use|already\s+linked\s+to\s+the\s+maximum\s+number\s+of\s+accounts|phone\s+number\s+is\s+already\s+(?:in\s+use|linked|registered)|phone\s+number\s+has\s+already\s+been\s+used|already\s+associated\s+with\s+another\s+account|not\s+eligible\s+to\s+be\s+used|cannot\s+be\s+used\s+for\s+verification|号码.*(?:已|被).*(?:使用|占用|绑定|注册)|手机号.*(?:已|被).*(?:使用|占用|绑定|注册)|电话.*(?:已|被).*(?:使用|占用|绑定|注册)|该手机号.*(?:已|被).*(?:使用|占用|绑定|注册)|该电话.*(?:已|被).*(?:使用|占用|绑定|注册)/i.test(text);
}
function isPhoneNumberDeliveryRefusedError(value) {
const text = String(value || '').trim();
if (!text) return false;
return /无法向此电话号码发送验证码|无法向.*(?:电话号码|手机号|号码).*发送(?:验证码|短信)|(?:不能|无法).*发送.*(?:验证码|短信).*(?:电话号码|手机号|号码)|(?:cannot|can't|could\s*not|couldn't|unable\s+to)\s+(?:send|deliver).{0,80}(?:verification\s+code|code|sms|text(?:\s+message)?).{0,80}(?:phone|number)|(?:verification\s+code|sms|text(?:\s+message)?).{0,80}(?:cannot|can't|could\s*not|couldn't|unable\s+to).{0,80}(?:send|deliver)/i.test(text);
}
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';
}
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;
return {
...DEFAULT_SETTINGS,
...input,
apiKey: asText(input.apiKey),
baseUrl: asText(input.baseUrl, DEFAULT_SETTINGS.baseUrl),
serviceCode: asText(input.serviceCode, DEFAULT_SETTINGS.serviceCode).toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_SETTINGS.serviceCode,
minPrice: normalizePrice(input.minPrice) === null ? '' : String(normalizePrice(input.minPrice)),
maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)),
countries,
activeActivation: input.activeActivation && typeof input.activeActivation === 'object' ? input.activeActivation : null,
};
}
async function getSettings() {
const stored = await chrome.storage.local.get(Object.keys(DEFAULT_SETTINGS));
return normalizeSettings(stored);
}
async function saveSettings(update = {}) {
const next = normalizeSettings({ ...(await getSettings()), ...update });
await chrome.storage.local.set(next);
return next;
}
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));
});
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 {}
}
return t;
}
function describePayload(payload) {
if (typeof payload === 'string') return payload.trim();
if (payload && typeof payload === 'object') {
const direct = payload.message || payload.msg || payload.error || payload.title || payload.status;
if (direct) return String(direct).trim();
try { return JSON.stringify(payload); } catch {}
}
return String(payload || '').trim();
}
async function apiRequest(settings, query, label = 'SMSBower 请求') {
if (!settings.apiKey) throw new Error('请先填写 SMSBower 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)) {
throw new Error(`${label}失败:${message || response.status}`);
}
return payload;
}
function normalizeActivation(payload, fallback = {}) {
if (typeof payload === 'string') {
const m = payload.match(/^ACCESS_NUMBER\s*:\s*([^:]+)\s*:\s*(.+)$/i);
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 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),
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,
acquiredAt: Date.now(),
};
}
function collectPriceEntries(payload, entries = []) {
if (Array.isArray(payload)) { payload.forEach((x) => collectPriceEntries(x, entries)); return entries; }
if (!payload || typeof payload !== 'object') return entries;
const count = Number(payload.count ?? payload.qty ?? payload.Qty);
const price = Number(payload.cost ?? payload.price ?? payload.Price);
if (Number.isFinite(price) && price >= 0) entries.push({ cost: price, count: Number.isFinite(count) ? count : 0, inStock: !Number.isFinite(count) || count > 0 });
Object.entries(payload).forEach(([key, value]) => {
if (value && typeof value === 'object' && !Array.isArray(value)) {
const keyedPrice = Number(key);
const keyedCount = Number(value.count ?? value.qty ?? value.Qty);
if (/^\d+(?:\.\d+)?$/.test(String(key)) && String(key).includes('.') && Number.isFinite(keyedPrice) && keyedPrice >= 0 && !Number.isFinite(Number(value.cost ?? value.price ?? value.Price))) {
entries.push({ cost: keyedPrice, count: Number.isFinite(keyedCount) ? keyedCount : 0, inStock: !Number.isFinite(keyedCount) || keyedCount > 0 });
}
}
collectPriceEntries(value, entries);
});
return entries;
}
function filterPriceEntries(entries, settings) {
const min = normalizePrice(settings.minPrice);
const max = normalizePrice(settings.maxPrice);
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) {
const out = [];
for (let index = 0; index < settings.countries.length; index += 1) {
const country = settings.countries[index];
try {
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) });
}
}
return out
.filter((x) => Number.isFinite(x.price))
.sort((a, b) => (a.price - b.price) || (a.countryIndex - b.countryIndex));
}
async function releaseActivation(settings, activation, releaseAction = 'cancel', quiet = false) {
if (!activation?.activationId) return { ok: false, skipped: true, releaseAction };
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' ? '释放被拒号码' : '取消上一个号码激活');
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 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,
});
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
return activation;
}
async function fetchNextPhone(options = {}) {
const settings = await getSettings();
const previous = settings.activeActivation;
const reason = normalizeFailureReason(options.reason || 'manual_replace');
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('当前国家和价格范围内没有可用库存。');
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}`);
}
}
throw new Error(`获取手机号失败:${errors.join(' | ')}`);
}
function extractVerificationCode(raw = '') {
const text = describePayload(raw).replace(/^STATUS_OK\s*:\s*/i, '').trim();
const matched = text.match(/\b(\d{4,8})\b/);
return matched?.[1] || text;
}
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 message = describePayload(payload);
if (/^STATUS_OK\s*:/i.test(message)) {
const code = extractVerificationCode(message);
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
return { code, raw: message, activationId: activation.activationId, phoneNumber: activation.phoneNumber };
}
if (/^STATUS_WAIT_CODE\b/i.test(message)) throw new Error('验证码还没到,稍后再点“复制验证码”。');
if (/^STATUS_CANCEL\b/i.test(message)) throw new Error('当前激活已取消。');
throw new Error(`暂未获取到验证码:${message || '空响应'}`);
}
async function fetchCountries(settings) {
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}`) });
});
}
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();
}
}
chrome.action.onClicked.addListener(async (tab) => {
if (!tab?.id) return;
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);
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
const type = String(message?.type || '');
if (type === 'GET_SETTINGS') return { ok: true, settings: await getSettings(), fallbackCountries: FALLBACK_COUNTRIES };
if (type === 'SAVE_SETTINGS') return { ok: true, settings: await saveSettings(message.payload || {}) };
if (type === 'FETCH_COUNTRIES') return { ok: true, countries: await fetchCountries(await getSettings()) };
if (type === 'FETCH_NEXT_PHONE') return { ok: true, ...(await fetchNextPhone(message.payload || {})) };
if (type === 'ROTATE_PHONE') return { ok: true, ...(await fetchNextPhone(message.payload || {})) };
if (type === 'CLASSIFY_PHONE_ERROR') {
const text = String(message.payload?.text || '');
const reason = isPhoneNumberUsedError(text) ? 'phone_number_used' : (isPhoneNumberDeliveryRefusedError(text) ? 'phone_delivery_refused' : 'unknown');
return { ok: true, matched: reason !== 'unknown', reason, releaseAction: getPhoneReplacementReleaseAction(reason || text) };
}
if (type === 'FETCH_ACTIVE_CODE') return { ok: true, ...(await fetchActiveCode()) };
if (type === 'CANCEL_ACTIVE') {
const settings = await getSettings();
const result = await cancelActivation(settings, settings.activeActivation, false);
await saveSettings({ activeActivation: null });
return { ok: true, result };
}
throw new Error(`未知消息:${type}`);
})().then(sendResponse).catch((error) => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
});
+465
View File
@@ -0,0 +1,465 @@
(function smsBowerFloatingTool() {
if (window.__smsBowerFloatingToolInstalled) {
return;
}
window.__smsBowerFloatingToolInstalled = true;
const PANEL_ID = 'smsbower-floating-phone-tool';
const FALLBACK_COUNTRIES = [
{ id: 0, label: 'Russia' }, { id: 1, label: 'Ukraine' }, { id: 2, label: 'Kazakhstan' },
{ id: 3, label: 'China' }, { id: 4, label: 'Philippines' }, { id: 5, label: 'Myanmar' },
{ id: 6, label: 'Indonesia' }, { id: 7, label: 'Malaysia' }, { id: 10, label: 'Vietnam' },
{ id: 12, label: 'USA virtual' }, { id: 16, label: 'United Kingdom' }, { id: 22, label: 'India' },
{ id: 32, label: 'Romania' }, { id: 36, label: 'Canada' }, { id: 43, label: 'Germany' },
{ id: 52, label: 'Thailand' }, { id: 73, label: 'France' }, { id: 78, label: 'Turkey' },
{ id: 151, label: 'Japan' }, { id: 187, label: 'USA' }
];
const COUNTRY_ALIASES = {
0: '俄罗斯 俄国 Russia RU', 1: '乌克兰 Ukraine UA', 2: '哈萨克斯坦 Kazakhstan KZ',
3: '中国 China CN', 4: '菲律宾 Philippines PH', 5: '缅甸 Myanmar Burma MM',
6: '印度尼西亚 印尼 Indonesia ID', 7: '马来西亚 Malaysia MY', 10: '越南 Vietnam VN',
12: '美国虚拟 USA virtual United States US', 16: '英国 United Kingdom Britain GB UK',
22: '印度 India IN', 32: '罗马尼亚 Romania RO', 36: '加拿大 Canada CA',
43: '德国 Germany DE', 52: '泰国 Thailand TH', 73: '法国 France FR',
78: '土耳其 Turkey TR', 151: '日本 Japan JP', 187: '美国 USA United States US'
};
let allCountries = FALLBACK_COUNTRIES.slice();
let selectedCountries = [{ id: 6, label: 'Indonesia' }];
let latestSettings = null;
let panel = null;
let dragState = null;
let codePollToken = 0;
function stopCodePolling() {
codePollToken += 1;
}
function isVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
}
function scoreInput(el) {
const text = [el.type, el.name, el.id, el.placeholder, el.autocomplete, el.getAttribute('aria-label')].join(' ').toLowerCase();
let score = 0;
if (el === document.activeElement) score += 100;
if (/phone|mobile|tel|手机号|電話|电话|号码|number/.test(text)) score += 50;
if (el.type === 'tel') score += 40;
if (el.type === 'text' || !el.type) score += 5;
if (el.readOnly || el.disabled) score -= 1000;
if (!isVisible(el)) score -= 1000;
return score;
}
function findBestInput() {
const inputs = Array.from(document.querySelectorAll('input, textarea'))
.filter((el) => !panel || !panel.contains(el));
const ranked = inputs.map((el) => ({ el, score: scoreInput(el) })).sort((a, b) => b.score - a.score);
return ranked[0]?.score > -100 ? ranked[0].el : null;
}
function setValue(el, value) {
el.focus();
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
if (desc?.set) desc.set.call(el, value); else el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function fillPhone(phoneNumber) {
const input = findBestInput();
if (!input) throw new Error('当前页面没有找到可填写的输入框。请先点一下目标输入框再获取。');
setValue(input, phoneNumber);
return { phoneNumber, tag: input.tagName, name: input.name || '', id: input.id || '' };
}
function send(type, payload = {}) {
return chrome.runtime.sendMessage({ type, payload }).then((res) => {
if (!res?.ok) throw new Error(res?.error || '操作失败');
return res;
});
}
function normalizeCountry(country) {
const id = Math.floor(Number(country?.id));
return { id, label: String(country?.label || `Country #${id}`).trim() };
}
function sameCountry(a, b) {
return Number(a?.id) === Number(b?.id);
}
function getCountrySearchText(country = {}) {
const id = Math.floor(Number(country?.id));
return [country?.label, id, COUNTRY_ALIASES[id] || ''].join(' ').toLowerCase();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function $(selector) {
return panel?.querySelector(selector) || null;
}
function setStatus(text, isError = false) {
const node = $('.sb-status');
if (!node) return;
node.textContent = text;
node.style.color = isError ? '#fecaca' : '#bbf7d0';
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
} catch {}
}
function collectSettings() {
return {
apiKey: $('.sb-api-key')?.value.trim() || '',
baseUrl: $('.sb-base-url')?.value.trim() || '',
serviceCode: $('.sb-service-code')?.value.trim() || 'dr',
minPrice: $('.sb-min-price')?.value.trim() || '',
maxPrice: $('.sb-max-price')?.value.trim() || '',
countries: selectedCountries,
};
}
async function saveSettings() {
stopCodePolling();
const res = await send('SAVE_SETTINGS', collectSettings());
latestSettings = res.settings;
selectedCountries = (res.settings.countries || []).map(normalizeCountry);
renderCountries();
setStatus('设置已保存');
}
async function refreshCountries() {
stopCodePolling();
await saveSettings();
setStatus('正在刷新国家列表...');
const res = await send('FETCH_COUNTRIES');
allCountries = (res.countries || []).map(normalizeCountry).filter((c) => c.id >= 0);
renderCountries();
setStatus('国家列表已刷新');
}
async function fetchAndFill(options = {}) {
stopCodePolling();
const btn = $('.sb-fetch');
if (btn) btn.disabled = true;
try {
await saveSettings();
const reason = options.reason || 'manual_replace';
const releaseAction = options.releaseAction || (reason === 'phone_number_used' ? 'ban' : 'cancel');
setStatus(releaseAction === 'ban' ? '正在释放被拒号码并获取新号...' : '正在取消上一个激活并获取新号...');
const res = await send('FETCH_NEXT_PHONE', { reason, releaseAction });
const phone = res.activation.phoneNumber;
await copyText(phone);
fillPhone(phone);
$('.sb-current-phone').textContent = phone;
$('.sb-current-id').textContent = res.activation.activationId;
const codeNode = $('.sb-current-code');
if (codeNode) codeNode.textContent = '无';
setStatus(`已切换、填入并复制:${phone}`);
} catch (error) {
setStatus(error.message || String(error), true);
} finally {
if (btn) btn.disabled = false;
}
}
function getPageTextForPhoneError() {
const clone = document.body?.cloneNode(true);
if (!clone) return '';
clone.querySelector(`#${PANEL_ID}`)?.remove();
return String(clone.innerText || clone.textContent || '').slice(-12000);
}
async function detectAndRotate() {
const text = getPageTextForPhoneError();
const classified = await send('CLASSIFY_PHONE_ERROR', { text });
if (!classified.matched) {
setStatus('未检测到明确的手机号错误,可用“手动换号”。', true);
return;
}
await fetchAndFill({ reason: classified.reason, releaseAction: classified.releaseAction });
}
async function cancelActive() {
stopCodePolling();
try {
setStatus('正在取消当前激活...');
await send('CANCEL_ACTIVE');
$('.sb-current-phone').textContent = '无';
$('.sb-current-id').textContent = '无';
const codeNode = $('.sb-current-code');
if (codeNode) codeNode.textContent = '无';
setStatus('当前激活已取消');
} catch (error) {
setStatus(error.message || String(error), true);
}
}
async function fetchAndCopyCode() {
stopCodePolling();
const token = codePollToken;
const btn = $('.sb-code');
if (btn) {
btn.disabled = true;
btn.textContent = '获取中...';
}
try {
let round = 0;
while (token === codePollToken) {
round += 1;
setStatus(`正在查询验证码,第 ${round} 次...`);
try {
const res = await send('FETCH_ACTIVE_CODE');
if (token !== codePollToken) return;
const codeNode = $('.sb-current-code');
if (codeNode) codeNode.textContent = res.code || '无';
setStatus(`已获取验证码:${res.code}`);
return;
} catch (error) {
if (token !== codePollToken) return;
const message = error.message || String(error);
if (/未知消息\s*[:]\s*FETCH_ACTIVE_CODE/i.test(message)) {
throw new Error('后台脚本还是旧版,没加载验证码接口。请到扩展管理页点“重新加载”,然后刷新当前网页并重新打开悬浮窗。');
}
if (/验证码还没到|STATUS_WAIT_CODE|NO_CODE|WAIT/i.test(message)) {
setStatus(`验证码还没到,1 秒后继续查询...(第 ${round} 次)`);
await sleep(1000);
continue;
}
throw error;
}
}
} catch (error) {
if (token === codePollToken) setStatus(error.message || String(error), true);
} finally {
if (token === codePollToken && btn) {
btn.disabled = false;
btn.textContent = '获取验证码';
}
}
}
function renderCountries() {
const selectedBox = $('.sb-selected-countries');
const list = $('.sb-country-list');
const keyword = ($('.sb-country-search')?.value || '').trim().toLowerCase();
if (!selectedBox || !list) return;
selectedBox.innerHTML = '';
selectedCountries.forEach((country, index) => {
const chip = document.createElement('span');
chip.className = 'sb-chip';
chip.textContent = `${index + 1}. ${country.label} #${country.id}`;
const close = document.createElement('button');
close.type = 'button';
close.textContent = '×';
close.addEventListener('click', () => {
selectedCountries = selectedCountries.filter((x) => !sameCountry(x, country));
renderCountries();
});
chip.appendChild(close);
selectedBox.appendChild(chip);
});
list.innerHTML = '';
allCountries
.filter((c) => !keyword || getCountrySearchText(c).includes(keyword))
.slice(0, 120)
.forEach((country) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sb-country-item' + (selectedCountries.some((x) => sameCountry(x, country)) ? ' selected' : '');
btn.innerHTML = `<span>${escapeHtml(country.label)}</span><span>#${country.id}</span>`;
btn.addEventListener('click', () => {
if (selectedCountries.some((x) => sameCountry(x, country))) {
selectedCountries = selectedCountries.filter((x) => !sameCountry(x, country));
} else {
selectedCountries.push(country);
}
renderCountries();
});
list.appendChild(btn);
});
}
function escapeHtml(value) {
return String(value || '').replace(/[&<>'"]/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[ch]));
}
function injectStyles() {
if (document.getElementById('smsbower-floating-phone-tool-style')) return;
const style = document.createElement('style');
style.id = 'smsbower-floating-phone-tool-style';
style.textContent = `
#${PANEL_ID} { position: fixed; z-index: 2147483647; right: 18px; top: 80px; width: 390px; max-height: calc(100vh - 110px); overflow: auto; box-sizing: border-box; padding: 12px; border: 1px solid #334155; border-radius: 14px; background: #0f172a; color: #e5e7eb; box-shadow: 0 18px 60px rgba(0,0,0,.45); font: 13px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
#${PANEL_ID} * { box-sizing: border-box; }
#${PANEL_ID} .sb-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; cursor: move; user-select: none; }
#${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: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; }
#${PANEL_ID} .sb-country-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
#${PANEL_ID} button { padding: 7px 10px; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #1f2937; cursor: pointer; font: inherit; }
#${PANEL_ID} button:hover { background: #273449; }
#${PANEL_ID} button:disabled { opacity: .55; cursor: not-allowed; }
#${PANEL_ID} .sb-primary { background: #2563eb; border-color: #3b82f6; }
#${PANEL_ID} .sb-primary:hover { background: #1d4ed8; }
#${PANEL_ID} .sb-selected-countries { display: flex; flex-wrap: wrap; gap: 6px; min-height: 24px; margin: 7px 0; }
#${PANEL_ID} .sb-chip { display: inline-flex; gap: 5px; align-items: center; padding: 4px 7px; border-radius: 999px; background: #0e7490; color: #ecfeff; }
#${PANEL_ID} .sb-chip button { padding: 0 3px; border: 0; background: transparent; }
#${PANEL_ID} .sb-country-list { max-height: 150px; overflow: auto; border: 1px solid #253244; border-radius: 8px; }
#${PANEL_ID} .sb-country-item { display: flex; justify-content: space-between; gap: 8px; width: 100%; padding: 7px 9px; border: 0; border-bottom: 1px solid #1f2937; border-radius: 0; background: #0b1220; text-align: left; }
#${PANEL_ID} .sb-country-item.selected { background: #123044; color: #bae6fd; }
#${PANEL_ID} .sb-top-fetch { width: 100%; margin: 2px 0 10px; font-weight: 700; }
#${PANEL_ID} .sb-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 11px; }
#${PANEL_ID} .sb-code-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
#${PANEL_ID} .sb-current { margin-top: 10px; padding: 9px; border-radius: 10px; background: #020617; color: #cbd5e1; }
#${PANEL_ID} code { color: #67e8f9; user-select: all; }
#${PANEL_ID} .sb-hint { margin: 7px 0 0; color: #94a3b8; font-size: 12px; }
#${PANEL_ID} .sb-status { color: #bbf7d0; }
`;
document.documentElement.appendChild(style);
}
async function createPanel() {
injectStyles();
panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
<div class="sb-head">
<span class="sb-title">SMSBower 填号</span>
<button class="sb-close" type="button" title="隐藏">×</button>
</div>
<button class="sb-fetch sb-primary sb-top-fetch" type="button">获取并填入</button>
<label>API Key <input class="sb-api-key" type="password" placeholder="SMSBower API Key" /></label>
<label>API 地址 <input class="sb-base-url" type="url" /></label>
<div class="sb-row">
<label>项目 <input class="sb-service-code" type="text" value="dr" /></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>
</div>
<section class="sb-country-box">
<div class="sb-country-head"><strong>国家顺序</strong><button class="sb-refresh" type="button">刷新国家</button></div>
<input class="sb-country-search" type="search" placeholder="搜索国家 / ID" />
<div class="sb-selected-countries"></div>
<div class="sb-country-list"></div>
<p class="sb-hint">优先级:价低优先;同价按国家选择顺序。</p>
</section>
<div class="sb-actions">
<button class="sb-save" type="button">保存</button>
<button class="sb-rotate-detect" type="button">检测报错换号</button>
<button class="sb-rotate-used" type="button">已使用换号</button>
<button class="sb-cancel" type="button">取消当前</button>
</div>
<div class="sb-code-row">
<button class="sb-code sb-primary" type="button">获取验证码</button>
<button class="sb-copy-code" type="button">复制验证码</button>
</div>
<section class="sb-current">
<div>当前号码:<code class="sb-current-phone">无</code></div>
<div>激活ID<code class="sb-current-id">无</code></div>
<div>验证码:<code class="sb-current-code">无</code></div>
<div>状态:<span class="sb-status">就绪</span></div>
</section>
`;
document.documentElement.appendChild(panel);
bindPanelEvents();
await loadSettingsIntoPanel();
}
function bindPanelEvents() {
$('.sb-close').addEventListener('click', () => { panel.style.display = 'none'; });
$('.sb-save').addEventListener('click', saveSettings);
$('.sb-fetch').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' }));
$('.sb-rotate-detect').addEventListener('click', detectAndRotate);
$('.sb-rotate-used').addEventListener('click', () => fetchAndFill({ reason: 'phone_number_used', releaseAction: 'ban' }));
$('.sb-cancel').addEventListener('click', cancelActive);
$('.sb-code').addEventListener('click', fetchAndCopyCode);
$('.sb-copy-code').addEventListener('click', async () => {
const code = $('.sb-current-code')?.textContent || '';
if (code && code !== '无') {
await copyText(code);
setStatus(`验证码已复制:${code}`);
} else {
setStatus('当前没有验证码可复制,请先点“获取验证码”。', true);
}
});
$('.sb-refresh').addEventListener('click', refreshCountries);
$('.sb-country-search').addEventListener('input', renderCountries);
const head = $('.sb-head');
head.addEventListener('mousedown', (event) => {
if (event.target.closest('button')) return;
const rect = panel.getBoundingClientRect();
dragState = { x: event.clientX, y: event.clientY, left: rect.left, top: rect.top };
event.preventDefault();
});
window.addEventListener('mousemove', (event) => {
if (!dragState) return;
panel.style.left = `${Math.max(0, dragState.left + event.clientX - dragState.x)}px`;
panel.style.top = `${Math.max(0, dragState.top + event.clientY - dragState.y)}px`;
panel.style.right = 'auto';
});
window.addEventListener('mouseup', () => { dragState = null; });
}
async function loadSettingsIntoPanel() {
const res = await send('GET_SETTINGS');
latestSettings = res.settings;
allCountries = (res.fallbackCountries || FALLBACK_COUNTRIES).map(normalizeCountry);
selectedCountries = (latestSettings.countries || []).map(normalizeCountry);
$('.sb-api-key').value = latestSettings.apiKey || '';
$('.sb-base-url').value = latestSettings.baseUrl || '';
$('.sb-service-code').value = latestSettings.serviceCode || 'dr';
$('.sb-min-price').value = latestSettings.minPrice || '';
$('.sb-max-price').value = latestSettings.maxPrice || '';
if (latestSettings.activeActivation) {
$('.sb-current-phone').textContent = latestSettings.activeActivation.phoneNumber || '无';
$('.sb-current-id').textContent = latestSettings.activeActivation.activationId || '无';
const codeNode = $('.sb-current-code');
if (codeNode) codeNode.textContent = latestSettings.activeActivation.lastCode || '无';
}
renderCountries();
}
async function togglePanel() {
try {
if (!panel || !document.documentElement.contains(panel)) {
await createPanel();
} else {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
if (panel.style.display !== 'none') await loadSettingsIntoPanel();
}
} catch (error) {
alert(`SMSBower 悬浮窗打开失败:${error.message || error}`);
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
if (message?.type === 'FILL_PHONE') {
const phoneNumber = String(message.phoneNumber || '').trim();
if (!phoneNumber) throw new Error('缺少手机号。');
return { ok: true, ...fillPhone(phoneNumber) };
}
if (message?.type === 'TOGGLE_FLOAT_PANEL') {
await togglePanel();
return { ok: true };
}
return undefined;
})().then((result) => {
if (result !== undefined) sendResponse(result);
}).catch((error) => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
});
})();
+10
View File
@@ -0,0 +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.",
"permissions": ["activeTab", "scripting", "storage", "clipboardWrite"],
"host_permissions": ["https://smsbower.app/*", "https://smsbower.page/*", "https://smsbower.online/*"],
"background": { "service_worker": "background.js" },
"action": { "default_title": "SMSBower 悬浮填号" }
}
+21
View File
@@ -0,0 +1,21 @@
body { width: 430px; margin: 0; font: 13px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #e5e7eb; background: #0f172a; }
main { padding: 14px; } h1 { margin: 0 0 12px; font-size: 18px; }
label { display: grid; gap: 5px; margin: 8px 0; color: #cbd5e1; }
input { box-sizing: border-box; width: 100%; padding: 7px 9px; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #111827; outline: none; }
input:focus { border-color: #38bdf8; box-shadow: 0 0 0 2px rgba(56,189,248,.18); }
.row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.country-box { margin-top: 10px; padding: 10px; border: 1px solid #253244; border-radius: 10px; background: #111827; }
.country-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
button { padding: 7px 10px; border: 1px solid #334155; border-radius: 8px; color: #e5e7eb; background: #1f2937; cursor: pointer; }
button:hover { background: #273449; } button:disabled { opacity: .55; cursor: not-allowed; }
.primary { background: #2563eb; border-color: #3b82f6; } .primary:hover { background: #1d4ed8; }
.selected { display: flex; flex-wrap: wrap; gap: 6px; min-height: 25px; margin: 8px 0; }
.chip { display: inline-flex; gap: 5px; align-items: center; padding: 4px 7px; border-radius: 999px; background: #0e7490; color: #ecfeff; }
.chip button { padding: 0 3px; border: 0; background: transparent; }
.country-list { max-height: 170px; overflow: auto; border: 1px solid #253244; border-radius: 8px; }
.country-item { display: flex; justify-content: space-between; gap: 8px; width: 100%; padding: 7px 9px; border: 0; border-bottom: 1px solid #1f2937; border-radius: 0; background: #0b1220; text-align: left; }
.country-item.selected-item { background: #123044; color: #bae6fd; }
.country-id { color: #94a3b8; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.actions { display: grid; grid-template-columns: 1fr 1.4fr 1fr; gap: 8px; margin-top: 12px; }
.current { margin-top: 12px; padding: 10px; border-radius: 10px; background: #020617; color: #cbd5e1; }
code { color: #67e8f9; user-select: all; } .hint { margin: 7px 0 0; color: #94a3b8; font-size: 12px; }
+42
View File
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SMSBower 填号</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<main>
<h1>SMSBower 填号</h1>
<label>API Key <input id="api-key" type="password" placeholder="SMSBower API Key" /></label>
<label>API 地址 <input id="base-url" type="url" /></label>
<div class="row">
<label>项目 <input id="service-code" type="text" value="dr" /></label>
<label>最低价 <input id="min-price" type="number" min="0" step="0.0001" placeholder="可空" /></label>
<label>最高价 <input id="max-price" type="number" min="0" step="0.0001" placeholder="可空" /></label>
</div>
<section class="country-box">
<div class="country-head">
<strong>国家顺序</strong>
<button id="refresh-countries" type="button">刷新国家</button>
</div>
<input id="country-search" type="search" placeholder="搜索国家 / ID" />
<div id="selected-countries" class="selected"></div>
<div id="country-list" class="country-list"></div>
<p class="hint">拿号优先级:价低优先;同价按上面选中的国家顺序优先。</p>
</section>
<div class="actions">
<button id="save" type="button">保存设置</button>
<button id="fetch-fill" type="button" class="primary">获取并填入当前页</button>
<button id="cancel-active" type="button">取消当前激活</button>
</div>
<section class="current">
<div>当前号码:<code id="current-phone"></code></div>
<div>激活ID<code id="current-id"></code></div>
<div>状态:<span id="status">就绪</span></div>
</section>
</main>
<script src="popup.js"></script>
</body>
</html>
+147
View File
@@ -0,0 +1,147 @@
let allCountries = [];
let selectedCountries = [];
const $ = (id) => document.getElementById(id);
function send(type, payload = {}) {
return chrome.runtime.sendMessage({ type, payload }).then((res) => {
if (!res?.ok) throw new Error(res?.error || '操作失败');
return res;
});
}
function setStatus(text, isError = false) {
$('status').textContent = text;
$('status').style.color = isError ? '#fca5a5' : '#a7f3d0';
}
function normalizeCountry(country) {
const id = Math.floor(Number(country?.id));
return { id, label: String(country?.label || `Country #${id}`).trim() };
}
function sameCountry(a, b) { return Number(a?.id) === Number(b?.id); }
function renderSelected() {
const box = $('selected-countries');
box.innerHTML = '';
selectedCountries.forEach((country, index) => {
const chip = document.createElement('span');
chip.className = 'chip';
chip.textContent = `${index + 1}. ${country.label} #${country.id}`;
const close = document.createElement('button');
close.type = 'button';
close.textContent = '×';
close.addEventListener('click', () => { selectedCountries = selectedCountries.filter((x) => !sameCountry(x, country)); renderAll(); });
chip.appendChild(close);
box.appendChild(chip);
});
}
function renderCountryList() {
const keyword = $('country-search').value.trim().toLowerCase();
const list = $('country-list');
list.innerHTML = '';
allCountries
.filter((c) => !keyword || String(c.id).includes(keyword) || c.label.toLowerCase().includes(keyword))
.slice(0, 120)
.forEach((country) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'country-item' + (selectedCountries.some((x) => sameCountry(x, country)) ? ' selected-item' : '');
btn.innerHTML = `<span>${country.label}</span><span class="country-id">#${country.id}</span>`;
btn.addEventListener('click', () => {
if (selectedCountries.some((x) => sameCountry(x, country))) {
selectedCountries = selectedCountries.filter((x) => !sameCountry(x, country));
} else {
selectedCountries.push(country);
}
renderAll();
});
list.appendChild(btn);
});
}
function renderAll() { renderSelected(); renderCountryList(); }
function collectSettings() {
return {
apiKey: $('api-key').value.trim(),
baseUrl: $('base-url').value.trim(),
serviceCode: $('service-code').value.trim(),
minPrice: $('min-price').value.trim(),
maxPrice: $('max-price').value.trim(),
countries: selectedCountries,
};
}
async function saveSettings() {
const res = await send('SAVE_SETTINGS', collectSettings());
selectedCountries = res.settings.countries.map(normalizeCountry);
renderAll();
setStatus('设置已保存');
}
async function fillCurrentTab(phoneNumber) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) throw new Error('没有找到当前标签页。');
try {
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
} catch {}
const res = await chrome.tabs.sendMessage(tab.id, { type: 'FILL_PHONE', phoneNumber });
if (!res?.ok) throw new Error(res?.error || '填入页面失败。');
return res;
}
async function copyText(text) {
try { await navigator.clipboard.writeText(text); } catch {}
}
async function fetchAndFill(options = {}) {
$('fetch-fill').disabled = true;
try {
await saveSettings();
const releaseAction = options.releaseAction || 'cancel';
setStatus(releaseAction === 'ban' ? '正在释放被拒号码并按低价拿号...' : '正在取消上一个激活并按低价拿号...');
const res = await send('FETCH_NEXT_PHONE', { reason: options.reason || 'manual_replace', releaseAction });
const phone = res.activation.phoneNumber;
await copyText(phone);
setStatus('正在填入当前页面输入框...');
await fillCurrentTab(phone);
$('current-phone').textContent = phone;
$('current-id').textContent = res.activation.activationId;
setStatus(`已获取、已复制、已填入:${phone}`);
} catch (error) {
setStatus(error.message || String(error), true);
} finally {
$('fetch-fill').disabled = false;
}
}
async function cancelActive() {
try {
setStatus('正在取消当前激活...');
await send('CANCEL_ACTIVE');
$('current-phone').textContent = '无';
$('current-id').textContent = '无';
setStatus('当前激活已取消');
} catch (error) { setStatus(error.message || String(error), true); }
}
async function refreshCountries() {
try {
await saveSettings();
setStatus('正在刷新国家列表...');
const res = await send('FETCH_COUNTRIES');
allCountries = res.countries.map(normalizeCountry).filter((c) => c.id >= 0);
renderAll();
setStatus('国家列表已刷新');
} catch (error) { setStatus(error.message || String(error), true); }
}
async function init() {
const res = await send('GET_SETTINGS');
const settings = res.settings;
$('api-key').value = settings.apiKey || '';
$('base-url').value = settings.baseUrl || '';
$('service-code').value = settings.serviceCode || 'dr';
$('min-price').value = settings.minPrice || '';
$('max-price').value = settings.maxPrice || '';
selectedCountries = (settings.countries || []).map(normalizeCountry);
allCountries = (res.fallbackCountries || []).map(normalizeCountry);
if (settings.activeActivation) {
$('current-phone').textContent = settings.activeActivation.phoneNumber || '无';
$('current-id').textContent = settings.activeActivation.activationId || '无';
}
$('save').addEventListener('click', saveSettings);
$('fetch-fill').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' }));
$('cancel-active').addEventListener('click', cancelActive);
$('refresh-countries').addEventListener('click', refreshCountries);
$('country-search').addEventListener('input', renderCountryList);
renderAll();
}
init().catch((error) => setStatus(error.message || String(error), true));
+99
View File
@@ -0,0 +1,99 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function load(extra = {}) {
const sandbox = {
chrome: {
action: { onClicked: { addListener() {} } },
tabs: { async sendMessage() {} },
scripting: { async executeScript() {} },
runtime: { onMessage: { addListener() {} } },
storage: { local: { async get() { return {}; }, async set() {} } }
},
fetch: async () => { throw new Error('not mocked'); },
URL,
console,
Date,
Number,
String,
Object,
Array,
RegExp,
Math,
JSON,
Set,
...extra,
};
const fn = new Function('globalThis', 'self', `with (globalThis) { ${source}; return { collectPriceEntries, filterPriceEntries, normalizeActivation, extractVerificationCode, isPhoneNumberUsedError, isPhoneNumberDeliveryRefusedError, getPhoneReplacementReleaseAction, fetchNextPhone, normalizeSettings }; }`);
return fn(sandbox, sandbox);
}
test('collectPriceEntries parses keyed price inventory', () => {
const api = load();
const entries = api.collectPriceEntries({ 6: { dr: { '0.12': { count: 3 }, '0.20': { count: 0 } } } });
assert.deepEqual(entries.filter((x) => x.inStock).map((x) => x.cost), [0.12]);
});
test('filterPriceEntries respects min and max', () => {
const api = load();
const entries = [{ cost: 0.05, inStock: true }, { cost: 0.15, inStock: true }, { cost: 0.3, inStock: true }];
assert.deepEqual(api.filterPriceEntries(entries, { minPrice: '0.1', maxPrice: '0.2' }).map((x) => x.cost), [0.15]);
});
test('normalizeActivation parses ACCESS_NUMBER response', () => {
const api = load();
const activation = api.normalizeActivation('ACCESS_NUMBER:12345:628123456789', { countryId: 6, countryLabel: 'Indonesia' });
assert.equal(activation.activationId, '12345');
assert.equal(activation.phoneNumber, '628123456789');
assert.equal(activation.countryLabel, 'Indonesia');
});
test('extractVerificationCode parses STATUS_OK response', () => {
const api = load();
assert.equal(api.extractVerificationCode('STATUS_OK: Your code is 654321'), '654321');
assert.equal(api.extractVerificationCode('STATUS_OK:987654'), '987654');
});
test('FlowPilot-style phone failure classifiers choose release action', () => {
const api = load();
assert.equal(api.isPhoneNumberUsedError('该电话已被使用,请换一个号码'), true);
assert.equal(api.isPhoneNumberDeliveryRefusedError('无法向此电话号码发送验证码'), true);
assert.equal(api.getPhoneReplacementReleaseAction('phone_number_used'), 'ban');
assert.equal(api.getPhoneReplacementReleaseAction('phone_delivery_refused'), 'cancel');
});
test('fetchNextPhone releases previous activation then acquires replacement', async () => {
const requests = [];
const stored = {
apiKey: 'demo-key',
baseUrl: 'https://smsbower.app/stubs/handler_api.php',
serviceCode: 'dr',
minPrice: '0.1',
maxPrice: '0.3',
countries: [{ id: 6, label: 'Indonesia' }],
activeActivation: { activationId: 'old-activation', phoneNumber: '628111', countryId: 6 },
};
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);
const action = parsed.searchParams.get('action');
requests.push([action, parsed.searchParams.get('id'), parsed.searchParams.get('status'), parsed.searchParams.get('country')]);
if (action === 'setStatus') return new Response('ACCESS_CANCEL', { status: 200 });
if (action === 'getPricesV3') return new Response(JSON.stringify({ 6: { dr: { '0.2': { count: 2 } } } }), { status: 200 });
if (action === 'getNumberV2') return new Response(JSON.stringify({ activationId: 'new-activation', phoneNumber: '628222', countryCode: 6 }), { status: 200 });
throw new Error(`unexpected ${url}`);
},
Response,
});
const result = await api.fetchNextPhone({ reason: 'phone_number_used', releaseAction: 'ban' });
assert.equal(result.activation.activationId, 'new-activation');
assert.equal(result.releasePrevious.releaseAction, 'ban');
assert.equal(stored.activeActivation.previousActivationId, 'old-activation');
assert.deepEqual(requests, [
['setStatus', 'old-activation', '8', null],
['getPricesV3', null, null, '6'],
['getNumberV2', null, null, '6'],
]);
});