Files
smsbower-phone-fill-tool/content.js
T
2026-05-31 16:41:45 +08:00

536 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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;
let autoRotateObserver = null;
let autoRotateTimer = null;
let autoRotateInFlight = false;
let autoRotateEnabled = true;
let lastErroredPhoneNumber = '';
const AUTO_ROTATE_DEBOUNCE_MS = 800;
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 {
if (!options.skipSave) 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;
lastErroredPhoneNumber = '';
$('.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);
}
function updateAutoRotateToggle() {
const btn = $('.sb-auto-toggle');
if (!btn) return;
btn.textContent = autoRotateEnabled ? '自动切号:开' : '自动切号:关';
btn.classList.toggle('sb-auto-on', autoRotateEnabled);
}
function getCurrentPhoneNumber() {
const phone = $('.sb-current-phone')?.textContent || latestSettings?.activeActivation?.phoneNumber || '';
return String(phone).trim();
}
async function rotateFromClassifiedError(classified, source = 'manual') {
const currentPhone = getCurrentPhoneNumber();
if (autoRotateInFlight) return false;
if (source === 'auto' && (!currentPhone || currentPhone === '无')) return false;
if (source === 'auto' && currentPhone === lastErroredPhoneNumber) return false;
autoRotateInFlight = true;
try {
lastErroredPhoneNumber = currentPhone;
setStatus(source === 'auto'
? `当前号码 ${currentPhone} 触发${classified.reason === 'phone_number_used' ? '已使用' : '验证码发送失败'},正在自动切下一个号...`
: '检测到手机号错误,正在切号...');
await fetchAndFill({ reason: classified.reason, releaseAction: classified.releaseAction, skipSave: true });
return true;
} finally {
autoRotateInFlight = false;
}
}
async function detectAndRotate(source = 'manual') {
const text = getPageTextForPhoneError();
const classified = await send('CLASSIFY_PHONE_ERROR', { text });
if (!classified.matched) {
if (source !== 'auto') setStatus('未检测到明确的手机号错误。', true);
return false;
}
return rotateFromClassifiedError(classified, source);
}
function scheduleAutoRotateCheck() {
if (!autoRotateEnabled || autoRotateInFlight) return;
if (autoRotateTimer) clearTimeout(autoRotateTimer);
autoRotateTimer = setTimeout(() => {
autoRotateTimer = null;
detectAndRotate('auto').catch((error) => setStatus(error.message || String(error), true));
}, AUTO_ROTATE_DEBOUNCE_MS);
}
function startAutoRotateObserver() {
if (autoRotateObserver) return;
autoRotateObserver = new MutationObserver((mutations) => {
if (!autoRotateEnabled) return;
if (mutations.some((m) => m.target && panel && panel.contains(m.target))) return;
scheduleAutoRotateCheck();
});
if (document.body) {
autoRotateObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
}
window.addEventListener('focus', scheduleAutoRotateCheck);
}
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; }
#${PANEL_ID} .sb-auto-on { border-color: #22c55e; 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>
<button class="sb-auto-toggle sb-auto-on" type="button">自动切号:开</button>
<label>API Key <input class="sb-api-key" type="password" placeholder="SMSBower API Key" /></label>
<label>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();
updateAutoRotateToggle();
startAutoRotateObserver();
scheduleAutoRotateCheck();
}
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-auto-toggle').addEventListener('click', () => {
autoRotateEnabled = !autoRotateEnabled;
updateAutoRotateToggle();
setStatus(autoRotateEnabled ? '自动切号已开启' : '自动切号已关闭');
if (autoRotateEnabled) scheduleAutoRotateCheck();
});
$('.sb-rotate-detect').addEventListener('click', () => detectAndRotate('manual'));
$('.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;
});
})();