feat: auto rotate phone on detected errors
This commit is contained in:
@@ -2,9 +2,11 @@ SMSBower Phone Fill Tool
|
|||||||
|
|
||||||
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。点一次“获取并填入”就获取一个 SMSBower 手机号,自动填入当前页面最像手机号的输入框,并复制到剪贴板。再次获取会先释放上一个激活,再拿新号替换页面输入框。
|
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。点一次“获取并填入”就获取一个 SMSBower 手机号,自动填入当前页面最像手机号的输入框,并复制到剪贴板。再次获取会先释放上一个激活,再拿新号替换页面输入框。
|
||||||
|
|
||||||
本版已集成 FlowPilot 同款切号机制:
|
本版已集成 FlowPilot 同款自动切号机制:
|
||||||
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,可点“检测报错换号”自动识别,或直接点“已使用换号”;旧号按被拒原因释放,新号自动填入并复制。
|
- 悬浮窗打开后默认开启“自动切号:开”。
|
||||||
- 页面提示“无法向此电话号码发送验证码”等发送失败时,“检测报错换号”会按可恢复失败换号。
|
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,插件会自动识别并切换下一个号码;旧号按被拒原因释放,新号自动填入并复制。
|
||||||
|
- 页面提示“无法向此电话号码发送验证码”等发送失败时,插件会自动按可恢复失败换号。
|
||||||
|
- “检测报错换号”和“已使用换号”仍保留为手动兜底按钮。
|
||||||
- 仍然只负责填号/查码/复制,不自动点击目标页面继续按钮。
|
- 仍然只负责填号/查码/复制,不自动点击目标页面继续按钮。
|
||||||
|
|
||||||
安装/更新:
|
安装/更新:
|
||||||
@@ -20,9 +22,10 @@ SMSBower Phone Fill Tool
|
|||||||
4. 填 SMSBower API Key、项目代码、价格上下限、国家顺序。
|
4. 填 SMSBower API Key、项目代码、价格上下限、国家顺序。
|
||||||
5. 顶部“获取并填入”会立即拿号并填入。
|
5. 顶部“获取并填入”会立即拿号并填入。
|
||||||
6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。
|
6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。
|
||||||
7. 如果目标页面拒绝号码:
|
7. 如果目标页面拒绝号码,保持悬浮窗打开即可:插件会自动扫描页面错误文本并切换下一个号码。
|
||||||
- 先点“检测报错换号”,插件会扫描当前页面文本,识别“已使用/发送验证码失败”等错误并换号;
|
- “自动切号:开/关”可以临时启停自动扫描;
|
||||||
- 如果你已经确认是“号码已使用”,可直接点“已使用换号”。
|
- “检测报错换号”用于手动触发一次扫描;
|
||||||
|
- “已使用换号”用于你已经确认号码被使用时立即换号。
|
||||||
8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;如果 SMSBower 已收到 STATUS_OK,会显示验证码并停止。点获取新号、保存、刷新国家或取消当前会停止这轮验证码轮询。
|
8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;如果 SMSBower 已收到 STATUS_OK,会显示验证码并停止。点获取新号、保存、刷新国家或取消当前会停止这轮验证码轮询。
|
||||||
9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。
|
9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。
|
||||||
10. 国家搜索支持中文别名,例如搜“印度”会出现 India 和 Indonesia。
|
10. 国家搜索支持中文别名,例如搜“印度”会出现 India 和 Indonesia。
|
||||||
|
|||||||
+81
-6
@@ -29,6 +29,14 @@
|
|||||||
let panel = null;
|
let panel = null;
|
||||||
let dragState = null;
|
let dragState = null;
|
||||||
let codePollToken = 0;
|
let codePollToken = 0;
|
||||||
|
let autoRotateObserver = null;
|
||||||
|
let autoRotateTimer = null;
|
||||||
|
let autoRotateInFlight = false;
|
||||||
|
let autoRotateEnabled = true;
|
||||||
|
let lastAutoRotateSignature = '';
|
||||||
|
let lastAutoRotateAt = 0;
|
||||||
|
const AUTO_ROTATE_DEBOUNCE_MS = 800;
|
||||||
|
const AUTO_ROTATE_COOLDOWN_MS = 8000;
|
||||||
|
|
||||||
function stopCodePolling() {
|
function stopCodePolling() {
|
||||||
codePollToken += 1;
|
codePollToken += 1;
|
||||||
@@ -153,7 +161,7 @@
|
|||||||
const btn = $('.sb-fetch');
|
const btn = $('.sb-fetch');
|
||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await saveSettings();
|
if (!options.skipSave) await saveSettings();
|
||||||
const reason = options.reason || 'manual_replace';
|
const reason = options.reason || 'manual_replace';
|
||||||
const releaseAction = options.releaseAction || (reason === 'phone_number_used' ? 'ban' : 'cancel');
|
const releaseAction = options.releaseAction || (reason === 'phone_number_used' ? 'ban' : 'cancel');
|
||||||
setStatus(releaseAction === 'ban' ? '正在释放被拒号码并获取新号...' : '正在取消上一个激活并获取新号...');
|
setStatus(releaseAction === 'ban' ? '正在释放被拒号码并获取新号...' : '正在取消上一个激活并获取新号...');
|
||||||
@@ -180,14 +188,70 @@
|
|||||||
return String(clone.innerText || clone.textContent || '').slice(-12000);
|
return String(clone.innerText || clone.textContent || '').slice(-12000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectAndRotate() {
|
function updateAutoRotateToggle() {
|
||||||
|
const btn = $('.sb-auto-toggle');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.textContent = autoRotateEnabled ? '自动切号:开' : '自动切号:关';
|
||||||
|
btn.classList.toggle('sb-auto-on', autoRotateEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAutoRotateSignature(reason, text) {
|
||||||
|
const compact = String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
return `${reason}:${compact.slice(-220)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateFromClassifiedError(classified, source = 'manual') {
|
||||||
|
const text = getPageTextForPhoneError();
|
||||||
|
const signature = buildAutoRotateSignature(classified.reason, text);
|
||||||
|
const now = Date.now();
|
||||||
|
if (autoRotateInFlight) return false;
|
||||||
|
if (source === 'auto' && signature === lastAutoRotateSignature && now - lastAutoRotateAt < AUTO_ROTATE_COOLDOWN_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
autoRotateInFlight = true;
|
||||||
|
lastAutoRotateSignature = signature;
|
||||||
|
lastAutoRotateAt = now;
|
||||||
|
try {
|
||||||
|
setStatus(source === 'auto'
|
||||||
|
? `检测到${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 text = getPageTextForPhoneError();
|
||||||
const classified = await send('CLASSIFY_PHONE_ERROR', { text });
|
const classified = await send('CLASSIFY_PHONE_ERROR', { text });
|
||||||
if (!classified.matched) {
|
if (!classified.matched) {
|
||||||
setStatus('未检测到明确的手机号错误,可用“手动换号”。', true);
|
if (source !== 'auto') setStatus('未检测到明确的手机号错误。', true);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
await fetchAndFill({ reason: classified.reason, releaseAction: classified.releaseAction });
|
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() {
|
async function cancelActive() {
|
||||||
@@ -328,6 +392,7 @@
|
|||||||
#${PANEL_ID} code { color: #67e8f9; user-select: all; }
|
#${PANEL_ID} code { color: #67e8f9; user-select: all; }
|
||||||
#${PANEL_ID} .sb-hint { margin: 7px 0 0; color: #94a3b8; font-size: 12px; }
|
#${PANEL_ID} .sb-hint { margin: 7px 0 0; color: #94a3b8; font-size: 12px; }
|
||||||
#${PANEL_ID} .sb-status { color: #bbf7d0; }
|
#${PANEL_ID} .sb-status { color: #bbf7d0; }
|
||||||
|
#${PANEL_ID} .sb-auto-on { border-color: #22c55e; color: #bbf7d0; }
|
||||||
`;
|
`;
|
||||||
document.documentElement.appendChild(style);
|
document.documentElement.appendChild(style);
|
||||||
}
|
}
|
||||||
@@ -342,6 +407,7 @@
|
|||||||
<button class="sb-close" type="button" title="隐藏">×</button>
|
<button class="sb-close" type="button" title="隐藏">×</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="sb-fetch sb-primary sb-top-fetch" type="button">获取并填入</button>
|
<button class="sb-fetch sb-primary sb-top-fetch" type="button">获取并填入</button>
|
||||||
|
<button class="sb-auto-toggle sb-auto-on" type="button">自动切号:开</button>
|
||||||
<label>API Key <input class="sb-api-key" type="password" placeholder="SMSBower API Key" /></label>
|
<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>
|
<label>API 地址 <input class="sb-base-url" type="url" /></label>
|
||||||
<div class="sb-row">
|
<div class="sb-row">
|
||||||
@@ -376,13 +442,22 @@
|
|||||||
document.documentElement.appendChild(panel);
|
document.documentElement.appendChild(panel);
|
||||||
bindPanelEvents();
|
bindPanelEvents();
|
||||||
await loadSettingsIntoPanel();
|
await loadSettingsIntoPanel();
|
||||||
|
updateAutoRotateToggle();
|
||||||
|
startAutoRotateObserver();
|
||||||
|
scheduleAutoRotateCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindPanelEvents() {
|
function bindPanelEvents() {
|
||||||
$('.sb-close').addEventListener('click', () => { panel.style.display = 'none'; });
|
$('.sb-close').addEventListener('click', () => { panel.style.display = 'none'; });
|
||||||
$('.sb-save').addEventListener('click', saveSettings);
|
$('.sb-save').addEventListener('click', saveSettings);
|
||||||
$('.sb-fetch').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' }));
|
$('.sb-fetch').addEventListener('click', () => fetchAndFill({ reason: 'manual_replace', releaseAction: 'cancel' }));
|
||||||
$('.sb-rotate-detect').addEventListener('click', detectAndRotate);
|
$('.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-rotate-used').addEventListener('click', () => fetchAndFill({ reason: 'phone_number_used', releaseAction: 'ban' }));
|
||||||
$('.sb-cancel').addEventListener('click', cancelActive);
|
$('.sb-cancel').addEventListener('click', cancelActive);
|
||||||
$('.sb-code').addEventListener('click', fetchAndCopyCode);
|
$('.sb-code').addEventListener('click', fetchAndCopyCode);
|
||||||
|
|||||||
+16
-1
@@ -25,7 +25,7 @@ function load(extra = {}) {
|
|||||||
Set,
|
Set,
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
const fn = new Function('globalThis', 'self', `with (globalThis) { ${source}; return { collectPriceEntries, filterPriceEntries, normalizeActivation, extractVerificationCode, isPhoneNumberUsedError, isPhoneNumberDeliveryRefusedError, getPhoneReplacementReleaseAction, fetchNextPhone, normalizeSettings }; }`);
|
const fn = new Function('globalThis', 'self', `with (globalThis) { ${source}; return { collectPriceEntries, filterPriceEntries, normalizeActivation, extractVerificationCode, isPhoneNumberUsedError, isPhoneNumberDeliveryRefusedError, getPhoneReplacementReleaseAction, fetchNextPhone, normalizeSettings, buildUrl }; }`);
|
||||||
return fn(sandbox, sandbox);
|
return fn(sandbox, sandbox);
|
||||||
}
|
}
|
||||||
test('collectPriceEntries parses keyed price inventory', () => {
|
test('collectPriceEntries parses keyed price inventory', () => {
|
||||||
@@ -53,10 +53,25 @@ test('extractVerificationCode parses STATUS_OK response', () => {
|
|||||||
test('FlowPilot-style phone failure classifiers choose release action', () => {
|
test('FlowPilot-style phone failure classifiers choose release action', () => {
|
||||||
const api = load();
|
const api = load();
|
||||||
assert.equal(api.isPhoneNumberUsedError('该电话已被使用,请换一个号码'), true);
|
assert.equal(api.isPhoneNumberUsedError('该电话已被使用,请换一个号码'), true);
|
||||||
|
assert.equal(api.isPhoneNumberUsedError('手机号已绑定,请换一个号码'), true);
|
||||||
assert.equal(api.isPhoneNumberDeliveryRefusedError('无法向此电话号码发送验证码'), true);
|
assert.equal(api.isPhoneNumberDeliveryRefusedError('无法向此电话号码发送验证码'), true);
|
||||||
assert.equal(api.getPhoneReplacementReleaseAction('phone_number_used'), 'ban');
|
assert.equal(api.getPhoneReplacementReleaseAction('phone_number_used'), 'ban');
|
||||||
assert.equal(api.getPhoneReplacementReleaseAction('phone_delivery_refused'), 'cancel');
|
assert.equal(api.getPhoneReplacementReleaseAction('phone_delivery_refused'), 'cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildUrl keeps api_key and replacement queries explicit', () => {
|
||||||
|
const api = load();
|
||||||
|
const url = new URL(api.buildUrl(
|
||||||
|
{ baseUrl: 'https://smsbower.app/stubs/handler_api.php', apiKey: 'demo-key' },
|
||||||
|
{ action: 'getNumberV2', service: 'dr', country: 6, maxPrice: 0.2 }
|
||||||
|
));
|
||||||
|
assert.equal(url.searchParams.get('api_key'), 'demo-key');
|
||||||
|
assert.equal(url.searchParams.get('action'), 'getNumberV2');
|
||||||
|
assert.equal(url.searchParams.get('service'), 'dr');
|
||||||
|
assert.equal(url.searchParams.get('country'), '6');
|
||||||
|
assert.equal(url.searchParams.get('maxPrice'), '0.2');
|
||||||
|
});
|
||||||
|
|
||||||
test('fetchNextPhone releases previous activation then acquires replacement', async () => {
|
test('fetchNextPhone releases previous activation then acquires replacement', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const stored = {
|
const stored = {
|
||||||
|
|||||||
Reference in New Issue
Block a user