feat: add 5SIM provider
This commit is contained in:
+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 = [];
|
||||
|
||||
Reference in New Issue
Block a user