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
+73 -8
View File
@@ -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 = [];