feat: add 5SIM provider

This commit is contained in:
chick
2026-05-31 19:37:56 +08:00
parent 09ccc76e71
commit ccd392326d
5 changed files with 265 additions and 49 deletions
+61 -36
View File
@@ -1,42 +1,67 @@
SMS/Luban Phone Fill Tool SMS/Luban/5SIM Phone Fill Tool
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。支持 SMSBowerLuban SMS个接码平台;点一次“获取并填入”会获取手机号,自动填入当前页面最像手机号的输入框、复制到剪贴板,并自动点击页面上的下一步/发送验证码按钮。再次获取会先释放上一个激活,再拿新号替换页面输入框并自动提交。 用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。支持 SMSBowerLuban SMS、5SIM 三个接码平台;点一次“获取并填入”会获取手机号,自动填入当前页面最像手机号的输入框、复制到剪贴板,并自动点击页面上的下一步/发送验证码按钮。再次获取会先释放上一个激活/订单,再拿新号替换页面输入框并自动提交。
已集成功能: 已集成功能:
- 接码平台可选:SMSBower / Luban SMS - 接码平台可选:SMSBower / Luban SMS / 5SIM
- SMSBower 使用 `api_key``getPricesV3``getNumberV2``getStatus``setStatus` - 自动保存 API Key、平台、服务、价格、国家/产品等配置到 chrome.storage.local
- Luban SMS 使用 `apikey``getNumber``getSms``setStatus status=reject` - SMSBower:按多个国家 + 价格范围查询库存,价低优先获取号码
- 悬浮窗打开后默认开启“自动切号:开”。 - Luban SMS:按 service_id 获取号码,支持 service 列表价格过滤
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,只要确认是当前新号码触发的错误,插件会自动切换下一个号码。 - 5SIM:按 country/operator/product 获取号码,使用 Bearer Token 鉴权,支持 maxPrice
- 页面提示“无法向此电话号码发送验证码”等发送失败时,插件会自动按可恢复失败换号。 - 自动取消/释放旧号码后获取新号码
- “检测报错换号”和“已使用换号”仍保留为手动兜底按钮。 - 自动填入当前页面手机号输入框
- 填号后会自动寻找并点击目标页面上的“下一步/继续/发送验证码/提交”等按钮;如果找不到按钮,会尝试提交表单或发送 Enter。 - 自动复制手机号到剪贴板
- 自动点击下一步/继续/发送验证码/提交按钮
- 自动检测页面号码错误并按当前号码自动切换下一个号
- 验证码查询轮询:按钮点一次后每秒自动查,拿到验证码后停止
- 一键复制验证码
- 手动“检测报错换号”“已使用换号”“取消当前”兜底
接码平台配置:
1. SMSBower
- API KeySMSBower key
- SMSBower API 地址:默认 https://smsbower.app/stubs/handler_api.php
- SMSBower 项目:默认 dr
- 国家顺序:选择一个或多个国家
- 最低价/最高价:可空
2. Luban SMS
- API KeyLuban apikey
- Luban API 地址:默认 https://lubansms.com/v2/api
- Luban service_id:必填,从 Luban 控制台或 List 接口查询
- 最低价/最高价:可空;如果 List 能查到 service_id cost,会按范围过滤
3. 5SIM
- API Key5SIM token,插件会用 Authorization: Bearer <token>
- 5SIM API 地址:默认 https://5sim.net
- 5SIM 国家:默认 vietnam;可填官方 country slug,也可填 any
- 5SIM 运营商:默认 any
- 5SIM 产品:默认 openai
- 最高价:可空;对应官方 buy activation 接口 maxPrice
- 最低价:5SIM 官方购买接口不支持最低价;使用 5SIM 时请留空
5SIM API 对应关系:
- 买号:GET /v1/user/buy/activation/{country}/{operator}/{product}?maxPrice=...
- 查码:GET /v1/user/check/{id}
- 取消:GET /v1/user/cancel/{id}
- 拉黑:GET /v1/user/ban/{id}
自动切号逻辑:
- 默认开启“自动切号:开”
- 页面出现“该电话已被使用 / 手机号已绑定 / 无法向此电话号码发送验证码”等错误时,插件会自动释放当前号码、获取下一个号码、填入、复制并自动提交
- 判定按当前号码进行:新号码触发相同错误文案也会继续切号
- 同一号码重复触发同一轮错误时不会并发重复切号
安装/更新: 安装/更新:
1. 打开 Chrome/Edge 扩展管理页。 1. 打开 chrome://extensions/
2. 开启“开发者模式” 2. 开启“开发者模式”
3. 加载已解压的扩展,选择本目录 3. 加载已解压的扩展程序,选择本目录
4. 如果之前已经加载过,点扩展卡片上的“重新加载”。 4. 已安装旧版时,点“重新加载”,然后刷新目标网页
使用 测试
1. 打开任意网页,先点一下你要填写的输入框。 node --check background.js
2. 点浏览器工具栏里的“接码填号”扩展图标。 node --check content.js
3. 页面右上角会出现悬浮窗,可拖动。 node --check popup.js
4. 选择接码平台: python3 -m json.tool manifest.json
- SMSBower:填写 API Key、项目代码、价格上下限、国家顺序。 node --test tests/provider.test.js
- Luban SMS:填写 API Key 和 `service_id``service_id` 可在 Luban 控制台或 API 服务列表中查询。Luban 的国家/服务由 `service_id` 决定,所以国家选择会隐藏。
5. 顶部“获取并填入”会立即拿号、填入、复制,并自动提交/点击发送验证码。
6. 再点一次“获取并填入”会释放上一个激活,替换输入框为新手机号。
7. 如果目标页面拒绝号码,保持悬浮窗打开即可:插件会自动扫描页面错误文本并切换下一个号码。
- “自动切号:开/关”可以临时启停自动扫描;
- “检测报错换号”用于手动触发一次扫描;
- “已使用换号”用于你已经确认号码被使用时立即换号。
8. 点“获取验证码”后会每 1 秒查询一次当前激活的验证码状态;收到验证码后显示并停止。
9. 看到验证码后,点“复制验证码”会复制当前显示的验证码。
10. 点悬浮窗右上角 × 只是隐藏;再次点扩展图标会重新显示。
说明:
- “复制验证码”只复制当前已查询到的验证码,不会自动填验证码。
- SMSBower 拿号优先级:价格低优先;同价格按你选择国家的顺序优先。
- Luban SMS 官方文档基础地址为 `https://lubansms.com/v2/api/`,扩展会按接口自动拼接 `getNumber/getSms/setStatus/List`
- 本地验证:`node --test tests/provider.test.js`
+73 -8
View File
@@ -3,8 +3,12 @@ const DEFAULT_SETTINGS = Object.freeze({
apiKey: '', apiKey: '',
baseUrl: 'https://smsbower.app/stubs/handler_api.php', baseUrl: 'https://smsbower.app/stubs/handler_api.php',
lubanBaseUrl: 'https://lubansms.com/v2/api', lubanBaseUrl: 'https://lubansms.com/v2/api',
fiveSimBaseUrl: 'https://5sim.net',
serviceCode: 'dr', serviceCode: 'dr',
lubanServiceId: '', lubanServiceId: '',
fiveSimCountry: 'vietnam',
fiveSimOperator: 'any',
fiveSimProduct: 'openai',
minPrice: '', minPrice: '',
maxPrice: '', maxPrice: '',
countries: [{ id: 6, label: 'Indonesia' }], countries: [{ id: 6, label: 'Indonesia' }],
@@ -28,6 +32,9 @@ const PROVIDERS = Object.freeze({
'luban-sms': { 'luban-sms': {
id: 'luban-sms', name: 'Luban SMS', apiKeyParam: 'apikey', baseUrlKey: 'lubanBaseUrl', defaultBaseUrl: 'https://lubansms.com/v2/api', id: 'luban-sms', name: 'Luban SMS', apiKeyParam: 'apikey', baseUrlKey: 'lubanBaseUrl', defaultBaseUrl: 'https://lubansms.com/v2/api',
}, },
'5sim': {
id: '5sim', name: '5SIM', apiKeyParam: '', baseUrlKey: 'fiveSimBaseUrl', defaultBaseUrl: 'https://5sim.net',
},
}); });
function asText(value = '', fallback = '') { return String(value ?? '').trim() || fallback; } function asText(value = '', fallback = '') { return String(value ?? '').trim() || fallback; }
@@ -37,6 +44,10 @@ function normalizeCountryId(value, fallback = 6) {
const parsed = Math.floor(Number(value)); const parsed = Math.floor(Number(value));
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
} }
function normalizeFiveSimSlug(value, fallback = '') {
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
return normalized || fallback;
}
function normalizePrice(value) { function normalizePrice(value) {
if (value === undefined || value === null || String(value).trim() === '') return null; if (value === undefined || value === null || String(value).trim() === '') return null;
const n = Number(value); const n = Number(value);
@@ -72,8 +83,12 @@ function normalizeSettings(input = {}) {
apiKey: asText(input.apiKey), apiKey: asText(input.apiKey),
baseUrl: asText(input.baseUrl, DEFAULT_SETTINGS.baseUrl), baseUrl: asText(input.baseUrl, DEFAULT_SETTINGS.baseUrl),
lubanBaseUrl: asText(input.lubanBaseUrl, DEFAULT_SETTINGS.lubanBaseUrl), lubanBaseUrl: asText(input.lubanBaseUrl, DEFAULT_SETTINGS.lubanBaseUrl),
fiveSimBaseUrl: asText(input.fiveSimBaseUrl, DEFAULT_SETTINGS.fiveSimBaseUrl),
serviceCode: asText(input.serviceCode, DEFAULT_SETTINGS.serviceCode).toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_SETTINGS.serviceCode, serviceCode: asText(input.serviceCode, DEFAULT_SETTINGS.serviceCode).toLowerCase().replace(/[^a-z0-9_-]+/g, '') || DEFAULT_SETTINGS.serviceCode,
lubanServiceId: asText(input.lubanServiceId ?? input.serviceId ?? input.luban_service_id, DEFAULT_SETTINGS.lubanServiceId).replace(/[^0-9a-zA-Z_-]+/g, ''), lubanServiceId: asText(input.lubanServiceId ?? input.serviceId ?? input.luban_service_id, DEFAULT_SETTINGS.lubanServiceId).replace(/[^0-9a-zA-Z_-]+/g, ''),
fiveSimCountry: normalizeFiveSimSlug(input.fiveSimCountry, DEFAULT_SETTINGS.fiveSimCountry),
fiveSimOperator: normalizeFiveSimSlug(input.fiveSimOperator, DEFAULT_SETTINGS.fiveSimOperator),
fiveSimProduct: normalizeFiveSimSlug(input.fiveSimProduct, DEFAULT_SETTINGS.fiveSimProduct),
minPrice: normalizePrice(input.minPrice) === null ? '' : String(normalizePrice(input.minPrice)), minPrice: normalizePrice(input.minPrice) === null ? '' : String(normalizePrice(input.minPrice)),
maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)), maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)),
countries, countries,
@@ -104,9 +119,13 @@ function buildUrl(settings, query = {}) {
const root = base.replace(/\/+(getBalance|getNumber|getSms|setStatus|getAgainNmuber|countries|List|smsHistory|getKeywordNumber|getKeywordSms|delKeywordNumber|keywordSmsHistory|bulksms)\/?$/i, ''); const root = base.replace(/\/+(getBalance|getNumber|getSms|setStatus|getAgainNmuber|countries|List|smsHistory|getKeywordNumber|getKeywordSms|delKeywordNumber|keywordSmsHistory|bulksms)\/?$/i, '');
base = `${root.replace(/\/+$/, '')}/${endpoint}`; base = `${root.replace(/\/+$/, '')}/${endpoint}`;
} }
} else if (provider.id === '5sim') {
const path = String(queryEntries.__path || '').replace(/^\/+/, '');
delete queryEntries.__path;
if (path) base = `${base.replace(/\/+$/, '')}/${path}`;
} }
const url = new URL(base); const url = new URL(base);
if (settings.apiKey) url.searchParams.set(provider.apiKeyParam, settings.apiKey); if (settings.apiKey && provider.apiKeyParam) url.searchParams.set(provider.apiKeyParam, settings.apiKey);
Object.entries(queryEntries).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v)); }); Object.entries(queryEntries).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v)); });
return url.toString(); return url.toString();
} }
@@ -128,7 +147,9 @@ function describePayload(payload) {
async function apiRequest(settings, query, label = '接码平台请求') { async function apiRequest(settings, query, label = '接码平台请求') {
const provider = getProvider(settings); const provider = getProvider(settings);
if (!settings.apiKey) throw new Error(`请先填写 ${provider.name} API Key。`); if (!settings.apiKey) throw new Error(`请先填写 ${provider.name} API Key。`);
const response = await fetch(buildUrl(settings, query), { method: 'GET', headers: { Accept: 'application/json, text/plain, */*' } }); const headers = { Accept: 'application/json, text/plain, */*' };
if (provider.id === '5sim') headers.Authorization = `Bearer ${settings.apiKey}`;
const response = await fetch(buildUrl(settings, query), { method: 'GET', headers });
const payload = parsePayload(await response.text()); const payload = parsePayload(await response.text());
const message = describePayload(payload); const message = describePayload(payload);
const apiStatus = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.status) : null; const apiStatus = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.status) : null;
@@ -147,12 +168,14 @@ function normalizeActivation(payload, fallback = {}) {
const activationId = asText(src.activationId ?? src.activation_id ?? src.request_id ?? src.id ?? fallback.activationId); const activationId = asText(src.activationId ?? src.activation_id ?? src.request_id ?? src.id ?? fallback.activationId);
const phoneNumber = asText(src.phoneNumber ?? src.phone_number ?? src.phone ?? src.number ?? fallback.phoneNumber); const phoneNumber = asText(src.phoneNumber ?? src.phone_number ?? src.phone ?? src.number ?? fallback.phoneNumber);
if (!activationId || !phoneNumber) return null; if (!activationId || !phoneNumber) return null;
const countryId = normalizeCountryId(src.countryId ?? src.country_id ?? src.country ?? fallback.countryId, fallback.countryId ?? 6); const provider = fallback.provider || 'sms-bower';
const rawCountry = src.countryId ?? src.country_id ?? src.country ?? fallback.countryId;
const countryId = provider === '5sim' ? normalizeFiveSimSlug(rawCountry, fallback.countryId || DEFAULT_SETTINGS.fiveSimCountry) : normalizeCountryId(rawCountry, fallback.countryId ?? 6);
return { return {
activationId, phoneNumber, provider: fallback.provider || 'sms-bower', activationId, phoneNumber, provider,
serviceCode: asText(src.serviceCode ?? src.service_id ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode), serviceCode: asText(src.serviceCode ?? src.service_id ?? src.product ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode),
countryId, countryId,
countryLabel: asText(src.countryLabel ?? src.country_name_zh ?? src.country_name_en ?? fallback.countryLabel, fallback.countryLabel || `Country #${countryId}`), countryLabel: asText(src.countryLabel ?? src.country_name_zh ?? src.country_name_en ?? src.country ?? fallback.countryLabel, provider === '5sim' ? countryId : `Country #${countryId}`),
price: Number.isFinite(Number(src.price ?? src.cost ?? fallback.price)) ? Number(src.price ?? src.cost ?? fallback.price) : null, price: Number.isFinite(Number(src.price ?? src.cost ?? fallback.price)) ? Number(src.price ?? src.cost ?? fallback.price) : null,
acquiredAt: Date.now(), acquiredAt: Date.now(),
}; };
@@ -206,8 +229,19 @@ async function fetchLubanCandidates(settings) {
if (normalizePrice(settings.maxPrice) !== null && price !== null && price > normalizePrice(settings.maxPrice)) return []; if (normalizePrice(settings.maxPrice) !== null && price !== null && price > normalizePrice(settings.maxPrice)) return [];
return [{ country: { id: -1, label: 'Luban service' }, countryIndex: 0, price: price ?? 0, count: 1, serviceId: settings.lubanServiceId }]; return [{ country: { id: -1, label: 'Luban service' }, countryIndex: 0, price: price ?? 0, count: 1, serviceId: settings.lubanServiceId }];
} }
async function fetchFiveSimCandidates(settings) {
const country = normalizeFiveSimSlug(settings.fiveSimCountry, DEFAULT_SETTINGS.fiveSimCountry);
const operator = normalizeFiveSimSlug(settings.fiveSimOperator, DEFAULT_SETTINGS.fiveSimOperator);
const product = normalizeFiveSimSlug(settings.fiveSimProduct, DEFAULT_SETTINGS.fiveSimProduct);
if (!country || !operator || !product) throw new Error('请先填写 5SIM 国家、运营商和产品。');
const max = normalizePrice(settings.maxPrice);
return [{ country: { id: country, label: country }, countryIndex: 0, price: max ?? 0, count: 1, operator, product }];
}
async function fetchPriceCandidates(settings) { async function fetchPriceCandidates(settings) {
return getProvider(settings).id === 'luban-sms' ? fetchLubanCandidates(settings) : fetchSmsBowerCandidates(settings); const providerId = getProvider(settings).id;
if (providerId === 'luban-sms') return fetchLubanCandidates(settings);
if (providerId === '5sim') return fetchFiveSimCandidates(settings);
return fetchSmsBowerCandidates(settings);
} }
async function releaseActivation(settings, activation, releaseAction = 'cancel', quiet = false) { async function releaseActivation(settings, activation, releaseAction = 'cancel', quiet = false) {
if (!activation?.activationId) return { ok: false, skipped: true, releaseAction }; if (!activation?.activationId) return { ok: false, skipped: true, releaseAction };
@@ -216,6 +250,7 @@ async function releaseActivation(settings, activation, releaseAction = 'cancel',
try { try {
let payload; let payload;
if (provider.id === 'luban-sms') payload = await apiRequest(settings, { __endpoint: 'setStatus', request_id: activation.activationId, status: 'reject' }, '释放 Luban 号码'); if (provider.id === 'luban-sms') payload = await apiRequest(settings, { __endpoint: 'setStatus', request_id: activation.activationId, status: 'reject' }, '释放 Luban 号码');
else if (provider.id === '5sim') payload = await apiRequest(settings, { __path: `v1/user/${normalizedAction === 'ban' ? 'ban' : 'cancel'}/${encodeURIComponent(activation.activationId)}` }, normalizedAction === 'ban' ? '5SIM 拉黑号码' : '5SIM 取消订单');
else payload = await apiRequest(settings, { action: 'setStatus', status: 8, id: activation.activationId }, normalizedAction === 'ban' ? '释放被拒号码' : '取消上一个号码激活'); else payload = await apiRequest(settings, { action: 'setStatus', status: 8, id: activation.activationId }, normalizedAction === 'ban' ? '释放被拒号码' : '取消上一个号码激活');
return { ok: true, payload: describePayload(payload), releaseAction: normalizedAction, activationId: activation.activationId }; return { ok: true, payload: describePayload(payload), releaseAction: normalizedAction, activationId: activation.activationId };
} catch (error) { } catch (error) {
@@ -233,6 +268,19 @@ async function requestActivation(settings, candidate) {
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`); if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
return activation; return activation;
} }
if (provider.id === '5sim') {
if (normalizePrice(settings.minPrice) !== null) throw new Error('5SIM 官方购买接口只支持 maxPrice,不支持最低价;请清空最低价。');
const query = {};
const maxPrice = normalizePrice(settings.maxPrice);
if (maxPrice !== null) query.maxPrice = maxPrice;
const payload = await apiRequest(settings, {
__path: `v1/user/buy/activation/${encodeURIComponent(candidate.country.id)}/${encodeURIComponent(candidate.operator)}/${encodeURIComponent(candidate.product)}`,
...query,
}, `购买 5SIM 手机号 ${candidate.country.label}`);
const activation = normalizeActivation(payload, { provider: provider.id, serviceCode: candidate.product, price: payload?.price ?? candidate.price, countryId: candidate.country.id, countryLabel: candidate.country.label });
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
return activation;
}
const payload = await apiRequest(settings, { action: 'getNumberV2', service: settings.serviceCode, country: candidate.country.id, minPrice: normalizePrice(settings.minPrice), maxPrice: Number.isFinite(candidate.price) ? candidate.price : normalizePrice(settings.maxPrice) }, `购买手机号 ${candidate.country.label}`); const payload = await apiRequest(settings, { action: 'getNumberV2', service: settings.serviceCode, country: candidate.country.id, minPrice: normalizePrice(settings.minPrice), maxPrice: Number.isFinite(candidate.price) ? candidate.price : normalizePrice(settings.maxPrice) }, `购买手机号 ${candidate.country.label}`);
const activation = normalizeActivation(payload, { provider: provider.id, countryId: candidate.country.id, countryLabel: candidate.country.label, serviceCode: settings.serviceCode, price: candidate.price }); const activation = normalizeActivation(payload, { provider: provider.id, countryId: candidate.country.id, countryLabel: candidate.country.label, serviceCode: settings.serviceCode, price: candidate.price });
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`); if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
@@ -268,7 +316,9 @@ async function fetchActiveCode() {
const provider = getProvider(settings); const provider = getProvider(settings);
const payload = provider.id === 'luban-sms' const payload = provider.id === 'luban-sms'
? await apiRequest(settings, { __endpoint: 'getSms', request_id: activation.activationId }, '查询 Luban 验证码') ? await apiRequest(settings, { __endpoint: 'getSms', request_id: activation.activationId }, '查询 Luban 验证码')
: await apiRequest(settings, { action: 'getStatus', id: activation.activationId }, '查询当前验证码'); : (provider.id === '5sim'
? await apiRequest(settings, { __path: `v1/user/check/${encodeURIComponent(activation.activationId)}` }, '查询 5SIM 验证码')
: await apiRequest(settings, { action: 'getStatus', id: activation.activationId }, '查询当前验证码'));
const message = describePayload(payload); const message = describePayload(payload);
if (provider.id === 'luban-sms') { if (provider.id === 'luban-sms') {
if (payload?.msg === 'success' && payload.sms_code) { if (payload?.msg === 'success' && payload.sms_code) {
@@ -279,6 +329,20 @@ async function fetchActiveCode() {
if (payload?.msg === 'wait') throw new Error('验证码还没到,稍后再点“复制验证码”。'); if (payload?.msg === 'wait') throw new Error('验证码还没到,稍后再点“复制验证码”。');
throw new Error(`暂未获取到验证码:${message || '空响应'}`); throw new Error(`暂未获取到验证码:${message || '空响应'}`);
} }
if (provider.id === '5sim') {
const smsList = Array.isArray(payload?.sms) ? payload.sms : [];
for (let index = smsList.length - 1; index >= 0; index -= 1) {
const item = smsList[index] || {};
const code = extractVerificationCode(item.code || item.text || '');
if (code) {
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
return { code, raw: message, activationId: activation.activationId, phoneNumber: activation.phoneNumber };
}
}
const status = String(payload?.status || '').toUpperCase();
if (['CANCELED', 'BANNED', 'FINISHED', 'TIMEOUT'].includes(status)) throw new Error(`5SIM 订单状态异常:${status}`);
throw new Error('验证码还没到,稍后再点“复制验证码”。');
}
if (/^STATUS_OK\s*:/i.test(message)) { if (/^STATUS_OK\s*:/i.test(message)) {
const code = extractVerificationCode(message); const code = extractVerificationCode(message);
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } }); await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
@@ -290,6 +354,7 @@ async function fetchActiveCode() {
} }
async function fetchCountries(settings) { async function fetchCountries(settings) {
if (getProvider(settings).id === 'luban-sms') return [{ id: -1, label: 'Luban service_id 模式' }]; if (getProvider(settings).id === 'luban-sms') return [{ id: -1, label: 'Luban service_id 模式' }];
if (getProvider(settings).id === '5sim') return [{ id: settings.fiveSimCountry || DEFAULT_SETTINGS.fiveSimCountry, label: '5SIM country slug 模式' }];
try { try {
const payload = await apiRequest(settings, { action: 'getCountries' }, '获取国家列表'); const payload = await apiRequest(settings, { action: 'getCountries' }, '获取国家列表');
const list = []; const list = [];
+28 -2
View File
@@ -181,8 +181,12 @@
apiKey: $('.sb-api-key')?.value.trim() || '', apiKey: $('.sb-api-key')?.value.trim() || '',
baseUrl: $('.sb-base-url')?.value.trim() || '', baseUrl: $('.sb-base-url')?.value.trim() || '',
lubanBaseUrl: $('.sb-luban-base-url')?.value.trim() || '', lubanBaseUrl: $('.sb-luban-base-url')?.value.trim() || '',
fiveSimBaseUrl: $('.sb-five-sim-base-url')?.value.trim() || '',
serviceCode: $('.sb-service-code')?.value.trim() || 'dr', serviceCode: $('.sb-service-code')?.value.trim() || 'dr',
lubanServiceId: $('.sb-luban-service-id')?.value.trim() || '', lubanServiceId: $('.sb-luban-service-id')?.value.trim() || '',
fiveSimCountry: $('.sb-five-sim-country')?.value.trim() || 'vietnam',
fiveSimOperator: $('.sb-five-sim-operator')?.value.trim() || 'any',
fiveSimProduct: $('.sb-five-sim-product')?.value.trim() || 'openai',
minPrice: $('.sb-min-price')?.value.trim() || '', minPrice: $('.sb-min-price')?.value.trim() || '',
maxPrice: $('.sb-max-price')?.value.trim() || '', maxPrice: $('.sb-max-price')?.value.trim() || '',
countries: selectedCountries, countries: selectedCountries,
@@ -370,6 +374,20 @@
} }
} }
function updateProviderFields() {
const provider = $('.sb-provider')?.value || 'sms-bower';
panel?.querySelectorAll('.sb-smsbower-only').forEach((el) => { el.style.display = provider === 'sms-bower' ? '' : 'none'; });
panel?.querySelectorAll('.sb-luban-only').forEach((el) => { el.style.display = provider === 'luban-sms' ? '' : 'none'; });
panel?.querySelectorAll('.sb-five-sim-only').forEach((el) => { el.style.display = provider === '5sim' ? '' : 'none'; });
const minPrice = $('.sb-min-price');
if (minPrice) {
minPrice.closest('label').style.display = provider === '5sim' ? 'none' : '';
if (provider === '5sim') minPrice.value = '';
}
const apiKey = $('.sb-api-key');
if (apiKey) apiKey.placeholder = provider === '5sim' ? '5SIM Bearer Token' : 'API Key / apikey';
}
function renderCountries() { function renderCountries() {
const selectedBox = $('.sb-selected-countries'); const selectedBox = $('.sb-selected-countries');
const list = $('.sb-country-list'); const list = $('.sb-country-list');
@@ -465,14 +483,18 @@
</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> <button class="sb-auto-toggle sb-auto-on" type="button">自动切号:开</button>
<label>接码平台 <select class="sb-provider"><option value="sms-bower">SMSBower</option><option value="luban-sms">Luban SMS</option></select></label> <label>接码平台 <select class="sb-provider"><option value="sms-bower">SMSBower</option><option value="luban-sms">Luban SMS</option><option value="5sim">5SIM</option></select></label>
<label>API Key <input class="sb-api-key" type="password" placeholder="API Key / apikey" /></label> <label>API Key <input class="sb-api-key" type="password" placeholder="API Key / apikey" /></label>
<label class="sb-smsbower-only">SMSBower API 地址 <input class="sb-base-url" type="url" /></label> <label class="sb-smsbower-only">SMSBower API 地址 <input class="sb-base-url" type="url" /></label>
<label class="sb-luban-only">Luban API 地址 <input class="sb-luban-base-url" type="url" /></label> <label class="sb-luban-only">Luban API 地址 <input class="sb-luban-base-url" type="url" /></label>
<label class="sb-five-sim-only">5SIM API 地址 <input class="sb-five-sim-base-url" type="url" /></label>
<div class="sb-row"> <div class="sb-row">
<label class="sb-smsbower-only">SMSBower 项目 <input class="sb-service-code" type="text" value="dr" /></label> <label class="sb-smsbower-only">SMSBower 项目 <input class="sb-service-code" type="text" value="dr" /></label>
<label class="sb-luban-only">Luban service_id <input class="sb-luban-service-id" type="text" placeholder="如 121949" /></label> <label class="sb-luban-only">Luban service_id <input class="sb-luban-service-id" type="text" placeholder="如 121949" /></label>
<label>最低价 <input class="sb-min-price" type="number" min="0" step="0.0001" placeholder="可空" /></label> <label class="sb-five-sim-only">5SIM 国家 <input class="sb-five-sim-country" type="text" placeholder="vietnam / any" /></label>
<label class="sb-five-sim-only">5SIM 运营商 <input class="sb-five-sim-operator" type="text" placeholder="any" /></label>
<label class="sb-five-sim-only">5SIM 产品 <input class="sb-five-sim-product" type="text" placeholder="openai" /></label>
<label>最低价 <input class="sb-min-price" type="number" min="0" step="0.0001" placeholder="5SIM 不支持" /></label>
<label>最高价 <input class="sb-max-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> </div>
<section class="sb-country-box sb-smsbower-only"> <section class="sb-country-box sb-smsbower-only">
@@ -558,8 +580,12 @@
$('.sb-api-key').value = latestSettings.apiKey || ''; $('.sb-api-key').value = latestSettings.apiKey || '';
$('.sb-base-url').value = latestSettings.baseUrl || ''; $('.sb-base-url').value = latestSettings.baseUrl || '';
$('.sb-luban-base-url').value = latestSettings.lubanBaseUrl || 'https://lubansms.com/v2/api'; $('.sb-luban-base-url').value = latestSettings.lubanBaseUrl || 'https://lubansms.com/v2/api';
$('.sb-five-sim-base-url').value = latestSettings.fiveSimBaseUrl || 'https://5sim.net';
$('.sb-service-code').value = latestSettings.serviceCode || 'dr'; $('.sb-service-code').value = latestSettings.serviceCode || 'dr';
$('.sb-luban-service-id').value = latestSettings.lubanServiceId || ''; $('.sb-luban-service-id').value = latestSettings.lubanServiceId || '';
$('.sb-five-sim-country').value = latestSettings.fiveSimCountry || 'vietnam';
$('.sb-five-sim-operator').value = latestSettings.fiveSimOperator || 'any';
$('.sb-five-sim-product').value = latestSettings.fiveSimProduct || 'openai';
$('.sb-min-price').value = latestSettings.minPrice || ''; $('.sb-min-price').value = latestSettings.minPrice || '';
$('.sb-max-price').value = latestSettings.maxPrice || ''; $('.sb-max-price').value = latestSettings.maxPrice || '';
if (latestSettings.activeActivation) { if (latestSettings.activeActivation) {
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "SMSBower Phone Fill Tool", "name": "SMSBower Phone Fill Tool",
"version": "1.5.0", "version": "1.6.0",
"description": "Fetch SMSBower or Luban SMS phone numbers, fill them into the current page, copy, auto-submit, rotate rejected numbers, and fetch codes.", "description": "Fetch SMSBower or Luban SMS phone numbers, fill them into the current page, copy, auto-submit, rotate rejected numbers, and fetch codes.",
"permissions": ["activeTab", "scripting", "storage", "clipboardWrite"], "permissions": ["activeTab", "scripting", "storage", "clipboardWrite"],
"host_permissions": ["https://smsbower.app/*", "https://smsbower.page/*", "https://smsbower.online/*", "https://lubansms.com/*"], "host_permissions": ["https://smsbower.app/*", "https://smsbower.page/*", "https://smsbower.online/*", "https://lubansms.com/*", "https://5sim.net/*"],
"background": { "service_worker": "background.js" }, "background": { "service_worker": "background.js" },
"action": { "default_title": "SMSBower 悬浮填号" } "action": { "default_title": "SMSBower 悬浮填号" }
} }
+101 -1
View File
@@ -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, buildUrl, fetchActiveCode }; }`); const fn = new Function('globalThis', 'self', `with (globalThis) { ${source}; return { collectPriceEntries, filterPriceEntries, normalizeActivation, extractVerificationCode, isPhoneNumberUsedError, isPhoneNumberDeliveryRefusedError, getPhoneReplacementReleaseAction, fetchNextPhone, normalizeSettings, buildUrl, fetchActiveCode, releaseActivation }; }`);
return fn(sandbox, sandbox); return fn(sandbox, sandbox);
} }
test('collectPriceEntries parses keyed price inventory', () => { test('collectPriceEntries parses keyed price inventory', () => {
@@ -202,3 +202,103 @@ test('fetchActiveCode parses Luban success SMS code', async () => {
assert.equal(result.code, '380682'); assert.equal(result.code, '380682');
assert.equal(stored.activeActivation.lastCode, '380682'); assert.equal(stored.activeActivation.lastCode, '380682');
}); });
test('buildUrl uses 5SIM bearer-token style URL without api_key query', () => {
const api = load();
const url = new URL(api.buildUrl(
{ provider: '5sim', fiveSimBaseUrl: 'https://5sim.net', apiKey: 'five-token' },
{ __path: 'v1/user/buy/activation/vietnam/any/openai', maxPrice: 0.2 }
));
assert.equal(url.origin, 'https://5sim.net');
assert.equal(url.pathname, '/v1/user/buy/activation/vietnam/any/openai');
assert.equal(url.searchParams.get('api_key'), null);
assert.equal(url.searchParams.get('apikey'), null);
assert.equal(url.searchParams.get('maxPrice'), '0.2');
});
test('fetchNextPhone acquires 5SIM number with Authorization bearer token', async () => {
const requests = [];
const stored = {
provider: '5sim',
apiKey: 'five-token',
fiveSimBaseUrl: 'https://5sim.net',
fiveSimCountry: 'vietnam',
fiveSimOperator: 'any',
fiveSimProduct: 'openai',
maxPrice: '0.25',
};
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, options = {}) => {
const parsed = new URL(url);
requests.push([parsed.pathname, parsed.searchParams.get('maxPrice'), options.headers.Authorization]);
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
return new Response(JSON.stringify({ id: 11631253, phone: '+84901234567', operator: 'viettel', product: 'openai', price: 0.2, status: 'PENDING', country: 'vietnam' }), { status: 200 });
}
throw new Error(`unexpected ${url}`);
},
Response,
});
const result = await api.fetchNextPhone({ reason: 'manual_replace', releaseAction: 'cancel' });
assert.equal(result.activation.provider, '5sim');
assert.equal(result.activation.activationId, '11631253');
assert.equal(result.activation.phoneNumber, '+84901234567');
assert.equal(stored.activeActivation.activationId, '11631253');
assert.deepEqual(requests, [['/v1/user/buy/activation/vietnam/any/openai', '0.25', 'Bearer five-token']]);
});
test('fetchActiveCode parses 5SIM order SMS code', async () => {
const stored = {
provider: '5sim',
apiKey: 'five-token',
fiveSimBaseUrl: 'https://5sim.net',
activeActivation: { activationId: '11631253', phoneNumber: '+84901234567', provider: '5sim' },
};
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, options = {}) => {
const parsed = new URL(url);
assert.equal(parsed.pathname, '/v1/user/check/11631253');
assert.equal(options.headers.Authorization, 'Bearer five-token');
return new Response(JSON.stringify({ id: 11631253, status: 'RECEIVED', sms: [{ text: 'Your code is 09363', code: '09363' }] }), { status: 200 });
},
Response,
});
const result = await api.fetchActiveCode();
assert.equal(result.code, '09363');
assert.equal(stored.activeActivation.lastCode, '09363');
});
test('releaseActivation cancels or bans 5SIM order using management endpoints', async () => {
const requests = [];
const stored = { provider: '5sim', apiKey: 'five-token', fiveSimBaseUrl: 'https://5sim.net' };
const api = load({
fetch: async (url, options = {}) => {
const parsed = new URL(url);
requests.push([parsed.pathname, options.headers.Authorization]);
return new Response(JSON.stringify({ id: 1001, status: parsed.pathname.includes('/ban/') ? 'BANNED' : 'CANCELED' }), { status: 200 });
},
Response,
});
const cancel = await api.releaseActivation(stored, { activationId: '1001' }, 'cancel');
const ban = await api.releaseActivation(stored, { activationId: '1002' }, 'ban');
assert.equal(cancel.ok, true);
assert.equal(ban.ok, true);
assert.deepEqual(requests, [
['/v1/user/cancel/1001', 'Bearer five-token'],
['/v1/user/ban/1002', 'Bearer five-token'],
]);
});