feat: add 5SIM provider
This commit is contained in:
@@ -1,42 +1,67 @@
|
||||
SMS/Luban Phone Fill Tool
|
||||
SMS/Luban/5SIM Phone Fill Tool
|
||||
|
||||
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。支持 SMSBower 和 Luban SMS 两个接码平台;点一次“获取并填入”会获取手机号,自动填入当前页面最像手机号的输入框、复制到剪贴板,并自动点击页面上的下一步/发送验证码按钮。再次获取会先释放上一个激活,再拿新号替换页面输入框并自动提交。
|
||||
用途:点击扩展图标后,在当前网页右上角打开一个常驻悬浮窗。支持 SMSBower、Luban SMS、5SIM 三个接码平台;点一次“获取并填入”会获取手机号,自动填入当前页面最像手机号的输入框、复制到剪贴板,并自动点击页面上的下一步/发送验证码按钮。再次获取会先释放上一个激活/订单,再拿新号替换页面输入框并自动提交。
|
||||
|
||||
已集成功能:
|
||||
- 接码平台可选:SMSBower / Luban SMS。
|
||||
- SMSBower 使用 `api_key`、`getPricesV3`、`getNumberV2`、`getStatus`、`setStatus`。
|
||||
- Luban SMS 使用 `apikey`、`getNumber`、`getSms`、`setStatus status=reject`。
|
||||
- 悬浮窗打开后默认开启“自动切号:开”。
|
||||
- 页面提示“该电话/手机号已被使用、已绑定、已注册”时,只要确认是当前新号码触发的错误,插件会自动切换下一个号码。
|
||||
- 页面提示“无法向此电话号码发送验证码”等发送失败时,插件会自动按可恢复失败换号。
|
||||
- “检测报错换号”和“已使用换号”仍保留为手动兜底按钮。
|
||||
- 填号后会自动寻找并点击目标页面上的“下一步/继续/发送验证码/提交”等按钮;如果找不到按钮,会尝试提交表单或发送 Enter。
|
||||
- 接码平台可选:SMSBower / Luban SMS / 5SIM
|
||||
- 自动保存 API Key、平台、服务、价格、国家/产品等配置到 chrome.storage.local
|
||||
- SMSBower:按多个国家 + 价格范围查询库存,价低优先获取号码
|
||||
- Luban SMS:按 service_id 获取号码,支持 service 列表价格过滤
|
||||
- 5SIM:按 country/operator/product 获取号码,使用 Bearer Token 鉴权,支持 maxPrice
|
||||
- 自动取消/释放旧号码后获取新号码
|
||||
- 自动填入当前页面手机号输入框
|
||||
- 自动复制手机号到剪贴板
|
||||
- 自动点击下一步/继续/发送验证码/提交按钮
|
||||
- 自动检测页面号码错误并按当前号码自动切换下一个号
|
||||
- 验证码查询轮询:按钮点一次后每秒自动查,拿到验证码后停止
|
||||
- 一键复制验证码
|
||||
- 手动“检测报错换号”“已使用换号”“取消当前”兜底
|
||||
|
||||
接码平台配置:
|
||||
|
||||
1. SMSBower
|
||||
- API Key:SMSBower key
|
||||
- SMSBower API 地址:默认 https://smsbower.app/stubs/handler_api.php
|
||||
- SMSBower 项目:默认 dr
|
||||
- 国家顺序:选择一个或多个国家
|
||||
- 最低价/最高价:可空
|
||||
|
||||
2. Luban SMS
|
||||
- API Key:Luban apikey
|
||||
- Luban API 地址:默认 https://lubansms.com/v2/api
|
||||
- Luban service_id:必填,从 Luban 控制台或 List 接口查询
|
||||
- 最低价/最高价:可空;如果 List 能查到 service_id cost,会按范围过滤
|
||||
|
||||
3. 5SIM
|
||||
- API Key:5SIM 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 扩展管理页。
|
||||
2. 开启“开发者模式”。
|
||||
3. 加载已解压的扩展,选择本目录。
|
||||
4. 如果之前已经加载过,点扩展卡片上的“重新加载”。
|
||||
1. 打开 chrome://extensions/
|
||||
2. 开启“开发者模式”
|
||||
3. 加载已解压的扩展程序,选择本目录
|
||||
4. 已安装旧版时,点“重新加载”,然后刷新目标网页
|
||||
|
||||
使用:
|
||||
1. 打开任意网页,先点一下你要填写的输入框。
|
||||
2. 点浏览器工具栏里的“接码填号”扩展图标。
|
||||
3. 页面右上角会出现悬浮窗,可拖动。
|
||||
4. 选择接码平台:
|
||||
- SMSBower:填写 API Key、项目代码、价格上下限、国家顺序。
|
||||
- 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`。
|
||||
测试:
|
||||
node --check background.js
|
||||
node --check content.js
|
||||
node --check popup.js
|
||||
python3 -m json.tool manifest.json
|
||||
node --test tests/provider.test.js
|
||||
|
||||
+73
-8
@@ -3,8 +3,12 @@ const DEFAULT_SETTINGS = Object.freeze({
|
||||
apiKey: '',
|
||||
baseUrl: 'https://smsbower.app/stubs/handler_api.php',
|
||||
lubanBaseUrl: 'https://lubansms.com/v2/api',
|
||||
fiveSimBaseUrl: 'https://5sim.net',
|
||||
serviceCode: 'dr',
|
||||
lubanServiceId: '',
|
||||
fiveSimCountry: 'vietnam',
|
||||
fiveSimOperator: 'any',
|
||||
fiveSimProduct: 'openai',
|
||||
minPrice: '',
|
||||
maxPrice: '',
|
||||
countries: [{ id: 6, label: 'Indonesia' }],
|
||||
@@ -28,6 +32,9 @@ const PROVIDERS = Object.freeze({
|
||||
'luban-sms': {
|
||||
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; }
|
||||
@@ -37,6 +44,10 @@ function normalizeCountryId(value, fallback = 6) {
|
||||
const parsed = Math.floor(Number(value));
|
||||
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) {
|
||||
if (value === undefined || value === null || String(value).trim() === '') return null;
|
||||
const n = Number(value);
|
||||
@@ -72,8 +83,12 @@ function normalizeSettings(input = {}) {
|
||||
apiKey: asText(input.apiKey),
|
||||
baseUrl: asText(input.baseUrl, DEFAULT_SETTINGS.baseUrl),
|
||||
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,
|
||||
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)),
|
||||
maxPrice: normalizePrice(input.maxPrice) === null ? '' : String(normalizePrice(input.maxPrice)),
|
||||
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, '');
|
||||
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);
|
||||
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)); });
|
||||
return url.toString();
|
||||
}
|
||||
@@ -128,7 +147,9 @@ function describePayload(payload) {
|
||||
async function apiRequest(settings, query, label = '接码平台请求') {
|
||||
const provider = getProvider(settings);
|
||||
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 message = describePayload(payload);
|
||||
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 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);
|
||||
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 {
|
||||
activationId, phoneNumber, provider: fallback.provider || 'sms-bower',
|
||||
serviceCode: asText(src.serviceCode ?? src.service_id ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode),
|
||||
activationId, phoneNumber, provider,
|
||||
serviceCode: asText(src.serviceCode ?? src.service_id ?? src.product ?? fallback.serviceCode, fallback.serviceCode || DEFAULT_SETTINGS.serviceCode),
|
||||
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,
|
||||
acquiredAt: Date.now(),
|
||||
};
|
||||
@@ -206,8 +229,19 @@ async function fetchLubanCandidates(settings) {
|
||||
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 }];
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
if (!activation?.activationId) return { ok: false, skipped: true, releaseAction };
|
||||
@@ -216,6 +250,7 @@ async function releaseActivation(settings, activation, releaseAction = 'cancel',
|
||||
try {
|
||||
let payload;
|
||||
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' ? '释放被拒号码' : '取消上一个号码激活');
|
||||
return { ok: true, payload: describePayload(payload), releaseAction: normalizedAction, activationId: activation.activationId };
|
||||
} catch (error) {
|
||||
@@ -233,6 +268,19 @@ async function requestActivation(settings, candidate) {
|
||||
if (!activation) throw new Error(`购买手机号返回不可用响应:${describePayload(payload)}`);
|
||||
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 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)}`);
|
||||
@@ -268,7 +316,9 @@ async function fetchActiveCode() {
|
||||
const provider = getProvider(settings);
|
||||
const payload = provider.id === 'luban-sms'
|
||||
? 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);
|
||||
if (provider.id === 'luban-sms') {
|
||||
if (payload?.msg === 'success' && payload.sms_code) {
|
||||
@@ -279,6 +329,20 @@ async function fetchActiveCode() {
|
||||
if (payload?.msg === 'wait') throw new Error('验证码还没到,稍后再点“复制验证码”。');
|
||||
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)) {
|
||||
const code = extractVerificationCode(message);
|
||||
await saveSettings({ activeActivation: { ...activation, lastCode: code, lastCodeRaw: message, lastCodeAt: Date.now() } });
|
||||
@@ -290,6 +354,7 @@ async function fetchActiveCode() {
|
||||
}
|
||||
async function fetchCountries(settings) {
|
||||
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 {
|
||||
const payload = await apiRequest(settings, { action: 'getCountries' }, '获取国家列表');
|
||||
const list = [];
|
||||
|
||||
+28
-2
@@ -181,8 +181,12 @@
|
||||
apiKey: $('.sb-api-key')?.value.trim() || '',
|
||||
baseUrl: $('.sb-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',
|
||||
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() || '',
|
||||
maxPrice: $('.sb-max-price')?.value.trim() || '',
|
||||
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() {
|
||||
const selectedBox = $('.sb-selected-countries');
|
||||
const list = $('.sb-country-list');
|
||||
@@ -465,14 +483,18 @@
|
||||
</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>接码平台 <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 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-five-sim-only">5SIM API 地址 <input class="sb-five-sim-base-url" type="url" /></label>
|
||||
<div class="sb-row">
|
||||
<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>最低价 <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>
|
||||
</div>
|
||||
<section class="sb-country-box sb-smsbower-only">
|
||||
@@ -558,8 +580,12 @@
|
||||
$('.sb-api-key').value = latestSettings.apiKey || '';
|
||||
$('.sb-base-url').value = latestSettings.baseUrl || '';
|
||||
$('.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-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-max-price').value = latestSettings.maxPrice || '';
|
||||
if (latestSettings.activeActivation) {
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"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.",
|
||||
"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" },
|
||||
"action": { "default_title": "SMSBower 悬浮填号" }
|
||||
}
|
||||
|
||||
+101
-1
@@ -25,7 +25,7 @@ function load(extra = {}) {
|
||||
Set,
|
||||
...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);
|
||||
}
|
||||
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(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'],
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user