diff --git a/phone-sms/providers/sms-bower.js b/phone-sms/providers/sms-bower.js index 64e5a63..7bfea2b 100644 --- a/phone-sms/providers/sms-bower.js +++ b/phone-sms/providers/sms-bower.js @@ -4,7 +4,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createSmsBowerProviderModule() { const PROVIDER_ID = 'sms-bower'; const DEFAULT_BASE_URL = 'https://smsbower.page/stubs/handler_api.php'; - const DEFAULT_SERVICE_CODE = 'ot'; + const DEFAULT_SERVICE_CODE = 'dr'; const DEFAULT_SERVICE_LABEL = 'OpenAI'; const DEFAULT_COUNTRY_ID = 6; const DEFAULT_COUNTRY_LABEL = 'Indonesia'; @@ -160,8 +160,9 @@ throw error; } const message = describePayload(payload); - if (/^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) { - const error = new Error(`${actionLabel}失败:${message}`); + const apiStatus = payload && typeof payload === 'object' && !Array.isArray(payload) ? Number(payload.status) : null; + if (apiStatus === 0 || /^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) { + const error = new Error(`${actionLabel}失败:${message || 'SMS Bower API 返回失败状态'}`); error.payload = payload; throw error; } @@ -220,7 +221,7 @@ async function fetchPrices(state = {}, countryConfig = {}, deps = {}) { const config = resolveConfig(state, deps); return fetchPayload(config, { - action: 'getPrices', + action: 'getPricesV3', service: config.serviceCode, country: normalizeSmsBowerCountryId(countryConfig?.id ?? state.smsBowerCountryId, DEFAULT_COUNTRY_ID), }, 'SMS Bower 查询价格'); @@ -232,12 +233,21 @@ return entries; } if (!payload || typeof payload !== 'object') return entries; - const cost = Number(payload.cost ?? payload.price ?? payload.Price); - if (Number.isFinite(cost) && cost >= 0) { - const count = Number(payload.count ?? payload.qty ?? payload.Qty); - entries.push({ cost, count: Number.isFinite(count) ? count : 0, inStock: !Number.isFinite(count) || count > 0 }); + const directCount = Number(payload.count ?? payload.qty ?? payload.Qty); + const directPrice = Number(payload.cost ?? payload.price ?? payload.Price); + if (Number.isFinite(directPrice) && directPrice >= 0) { + entries.push({ cost: directPrice, count: Number.isFinite(directCount) ? directCount : 0, inStock: !Number.isFinite(directCount) || directCount > 0 }); } - Object.values(payload).forEach((entry) => collectPriceEntries(entry, entries)); + Object.entries(payload).forEach(([key, entry]) => { + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + const keyedPrice = Number(key); + const keyedCount = Number(entry.count ?? entry.qty ?? entry.Qty); + if (/^\d+(?:\.\d+)?$/.test(String(key)) && String(key).includes('.') && Number.isFinite(keyedPrice) && keyedPrice >= 0 && !Number.isFinite(Number(entry.cost ?? entry.price ?? entry.Price))) { + entries.push({ cost: keyedPrice, count: Number.isFinite(keyedCount) ? keyedCount : 0, inStock: !Number.isFinite(keyedCount) || keyedCount > 0 }); + } + } + collectPriceEntries(entry, entries); + }); return entries; } @@ -249,10 +259,10 @@ } } const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}; - const activationId = normalizeText(source.activationId ?? source.id ?? fallback.activationId); - const phoneNumber = normalizeText(source.phoneNumber ?? source.phone ?? fallback.phoneNumber); + const activationId = normalizeText(source.activationId ?? source.activation_id ?? source.id ?? fallback.activationId); + const phoneNumber = normalizeText(source.phoneNumber ?? source.phone_number ?? source.phone ?? fallback.phoneNumber); if (!activationId || !phoneNumber) return null; - const countryId = normalizeSmsBowerCountryId(source.countryId ?? source.country ?? fallback.countryId, DEFAULT_COUNTRY_ID); + const countryId = normalizeSmsBowerCountryId(source.countryId ?? source.country_id ?? source.countryCode ?? source.country ?? fallback.countryId, DEFAULT_COUNTRY_ID); return { activationId, phoneNumber, @@ -287,7 +297,7 @@ throw new Error(`SMS Bower 价格区间无效:最低购买价 ${range.minPriceLimit} 高于价格上限 ${range.maxPriceLimit}。`); } return { - action: 'getNumber', + action: 'getNumberV2', service: config.serviceCode, country: normalizeSmsBowerCountryId(countryConfig.id, DEFAULT_COUNTRY_ID), maxPrice: range.maxPriceLimit, diff --git a/tests/sms-bower-provider.test.js b/tests/sms-bower-provider.test.js index 86fdb4f..f6cb128 100644 --- a/tests/sms-bower-provider.test.js +++ b/tests/sms-bower-provider.test.js @@ -14,13 +14,13 @@ function createTextResponse(payload, ok = true, status = ok ? 200 : 400, statusT }; } -test('SMS Bower provider defaults to Indonesia when no country is configured', async () => { +test('SMS Bower provider defaults to Indonesia and documented OpenAI service code', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); requests.push({ url: parsed, options }); - if (parsed.searchParams.get('action') === 'getNumber') { + if (parsed.searchParams.get('action') === 'getNumberV2') { return createTextResponse('ACCESS_NUMBER:67890:6281234567890'); } throw new Error(`unexpected ${parsed.toString()}`); @@ -30,11 +30,12 @@ test('SMS Bower provider defaults to Indonesia when no country is configured', a const activation = await provider.requestActivation({ smsBowerApiKey: 'demo-key' }); assert.equal(requests[0].url.searchParams.get('country'), '6'); + assert.equal(requests[0].url.searchParams.get('service'), 'dr'); assert.equal(activation.countryId, 6); assert.equal(activation.countryLabel, 'Indonesia'); }); -test('SMS Bower provider uses handler API key query for balance and price catalog', async () => { +test('SMS Bower provider uses documented handler API methods for balance and price catalog', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { @@ -43,8 +44,8 @@ test('SMS Bower provider uses handler API key query for balance and price catalo if (parsed.searchParams.get('action') === 'getBalance') { return createTextResponse('ACCESS_BALANCE:12.34'); } - if (parsed.searchParams.get('action') === 'getPrices') { - return createTextResponse({ 52: { ot: { cost: 0.21, count: 7 } } }); + if (parsed.searchParams.get('action') === 'getPricesV3') { + return createTextResponse({ 52: { dr: { 3170: { price: 0.21, count: 7 } } } }); } throw new Error(`unexpected ${parsed.toString()}`); }, @@ -58,21 +59,32 @@ test('SMS Bower provider uses handler API key query for balance and price catalo assert.equal(requests[0].url.searchParams.get('api_key'), 'demo-key'); assert.equal(requests[0].url.searchParams.get('action'), 'getBalance'); assert.equal(balance.balance, 12.34); - assert.equal(requests[1].url.searchParams.get('action'), 'getPrices'); - assert.equal(requests[1].url.searchParams.get('service'), 'ot'); + assert.equal(requests[1].url.searchParams.get('action'), 'getPricesV3'); + assert.equal(requests[1].url.searchParams.get('service'), 'dr'); assert.equal(requests[1].url.searchParams.get('country'), '52'); assert.deepStrictEqual(entries, [{ cost: 0.21, count: 7, inStock: true }]); }); -test('SMS Bower provider acquires number, polls code, and updates activation statuses', async () => { +test('SMS Bower provider treats JSON status 0 responses as API errors even when HTTP is ok', async () => { + const provider = api.createProvider({ + fetchImpl: async () => createTextResponse({ status: 0, message: 'No access', data: [] }, true, 200), + }); + + await assert.rejects( + () => provider.fetchBalance({ smsBowerApiKey: 'bad-key' }), + /SMS Bower 查询余额失败:No access/ + ); +}); + +test('SMS Bower provider acquires number with getNumberV2, polls code, and updates activation statuses', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); const action = parsed.searchParams.get('action'); requests.push({ url: parsed, options }); - if (action === 'getNumber') { - return createTextResponse('ACCESS_NUMBER:12345:447700900123'); + if (action === 'getNumberV2') { + return createTextResponse({ activationId: '12345', phoneNumber: '447700900123', activationCost: 0.3, countryCode: 16 }); } if (action === 'getStatus') { return createTextResponse('STATUS_OK:Your OpenAI code is 654321'); @@ -88,7 +100,7 @@ test('SMS Bower provider acquires number, polls code, and updates activation sta const state = { smsBowerApiKey: 'demo-key', - smsBowerServiceCode: 'ot', + smsBowerServiceCode: 'dr', smsBowerCountryId: 16, smsBowerCountryLabel: 'United Kingdom', smsBowerMinPrice: '0.1', @@ -110,8 +122,8 @@ test('SMS Bower provider acquires number, polls code, and updates activation sta assert.equal(reused.activationId, '12345'); const acquireUrl = requests[0].url; - assert.equal(acquireUrl.searchParams.get('action'), 'getNumber'); - assert.equal(acquireUrl.searchParams.get('service'), 'ot'); + assert.equal(acquireUrl.searchParams.get('action'), 'getNumberV2'); + assert.equal(acquireUrl.searchParams.get('service'), 'dr'); assert.equal(acquireUrl.searchParams.get('country'), '16'); assert.equal(acquireUrl.searchParams.get('maxPrice'), '0.3'); assert.equal(acquireUrl.searchParams.get('minPrice'), '0.1'); @@ -121,7 +133,7 @@ test('SMS Bower provider acquires number, polls code, and updates activation sta assert.deepStrictEqual( requests.map((entry) => [entry.url.searchParams.get('action'), entry.url.searchParams.get('status')]), [ - ['getNumber', null], + ['getNumberV2', null], ['getStatus', null], ['setStatus', '6'], ['setStatus', '8'],