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
+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;
});
})();