cloudtentacles 增加登陆后持久化, 调试ok

This commit is contained in:
yml2213
2026-05-02 23:01:02 +08:00
parent a5b93b17f0
commit 4cc91bf194
17 changed files with 1365 additions and 6 deletions
+50
View File
@@ -295,6 +295,56 @@ function applyEnvOverrides(baseConfig) {
nextConfig.platforms.cloudtentacles.permissionPath = cloudtentaclesPermissionPath
}
const cloudtentaclesCategoriesPath = String(process.env.CLOUDTENTACLES_CATEGORIES_PATH || '').trim()
if (cloudtentaclesCategoriesPath) {
nextConfig.platforms.cloudtentacles.categoriesPath = cloudtentaclesCategoriesPath
}
const cloudtentaclesSkuListPath = String(process.env.CLOUDTENTACLES_SKU_LIST_PATH || '').trim()
if (cloudtentaclesSkuListPath) {
nextConfig.platforms.cloudtentacles.skuListPath = cloudtentaclesSkuListPath
}
const cloudtentaclesSkuBuyPath = String(process.env.CLOUDTENTACLES_SKU_BUY_PATH || '').trim()
if (cloudtentaclesSkuBuyPath) {
nextConfig.platforms.cloudtentacles.skuBuyPath = cloudtentaclesSkuBuyPath
}
const cloudtentaclesKnapsackPath = String(process.env.CLOUDTENTACLES_KNAPSACK_PATH || '').trim()
if (cloudtentaclesKnapsackPath) {
nextConfig.platforms.cloudtentacles.knapsackPath = cloudtentaclesKnapsackPath
}
const cloudtentaclesVnListPath = String(process.env.CLOUDTENTACLES_VN_LIST_PATH || '').trim()
if (cloudtentaclesVnListPath) {
nextConfig.platforms.cloudtentacles.vnListPath = cloudtentaclesVnListPath
}
const cloudtentaclesVnAppointPath = String(process.env.CLOUDTENTACLES_VN_APPOINT_PATH || '').trim()
if (cloudtentaclesVnAppointPath) {
nextConfig.platforms.cloudtentacles.vnAppointPath = cloudtentaclesVnAppointPath
}
const cloudtentaclesVnGenerateLoginCodePath = String(process.env.CLOUDTENTACLES_VN_GENERATE_LOGIN_CODE_PATH || '').trim()
if (cloudtentaclesVnGenerateLoginCodePath) {
nextConfig.platforms.cloudtentacles.vnGenerateLoginCodePath = cloudtentaclesVnGenerateLoginCodePath
}
const cloudtentaclesVnVerifCodePath = String(process.env.CLOUDTENTACLES_VN_VERIF_CODE_PATH || '').trim()
if (cloudtentaclesVnVerifCodePath) {
nextConfig.platforms.cloudtentacles.vnVerifCodePath = cloudtentaclesVnVerifCodePath
}
const cloudtentaclesVnVerifyLoginCodePath = String(process.env.CLOUDTENTACLES_VN_VERIFY_LOGIN_CODE_PATH || '').trim()
if (cloudtentaclesVnVerifyLoginCodePath) {
nextConfig.platforms.cloudtentacles.vnVerifyLoginCodePath = cloudtentaclesVnVerifyLoginCodePath
}
const cloudtentaclesVnBindUrlPath = String(process.env.CLOUDTENTACLES_VN_BIND_URL_PATH || '').trim()
if (cloudtentaclesVnBindUrlPath) {
nextConfig.platforms.cloudtentacles.vnBindUrlPath = cloudtentaclesVnBindUrlPath
}
const cloudtentaclesPublicKeyPem = String(process.env.CLOUDTENTACLES_PUBLIC_KEY_PEM || '').trim()
if (cloudtentaclesPublicKeyPem) {
nextConfig.platforms.cloudtentacles.publicKeyPem = cloudtentaclesPublicKeyPem
@@ -3,10 +3,20 @@
import { Router } from 'express'
import {
appointAdminCloudtentaclesVirtualNumber,
buyAdminCloudtentaclesSku,
fetchAdminCloudtentaclesVirtualNumberCode,
generateAdminCloudtentaclesLoginCode,
getAdminCloudtentaclesAsset,
getAdminCloudtentaclesBindUrl,
getAdminCloudtentaclesCategories,
getAdminCloudtentaclesKnapsack,
getAdminCloudtentaclesSourceConfig,
getAdminCloudtentaclesSkuList,
getAdminAgisoShopConfigs,
getAdminFulfillmentBindingConfigs,
getAdminKhhaoSourceConfig,
listAdminCloudtentaclesVirtualNumbers,
lookupAdminFulfillmentBindingOrder,
queryAdminKhhaoOrders,
sendAdminCloudtentaclesSmsCode,
@@ -17,6 +27,7 @@ import {
updateAdminAgisoShopConfigs,
updateAdminKhhaoSourceConfig,
updateAdminFulfillmentBindingConfigs,
verifyAdminCloudtentaclesLoginCode,
validateAdminCloudtentaclesSession,
} from '../../services/admin/admin-platform-config-service.js'
import { createJsonHandler, requireAdminRoles } from './shared.js'
@@ -31,6 +42,9 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSendSmsCodeRouteBody} AdminCloudtentaclesSendSmsCodeRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesValidateSessionRouteBody} AdminCloudtentaclesValidateSessionRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesCatalogQueryRouteBody} AdminCloudtentaclesCatalogQueryRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSkuBuyRouteBody} AdminCloudtentaclesSkuBuyRouteBody */
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesVirtualNumberRouteBody} AdminCloudtentaclesVirtualNumberRouteBody */
/** @typedef {import('../../types/admin-write-models.js').AdminAgisoShopConfigSaveResponse} AdminAgisoShopConfigSaveResponse */
const router = Router()
@@ -268,6 +282,105 @@ router.post('/platform-config/cloudtentacles/validate-session', createJsonHandle
},
))
router.post('/platform-config/cloudtentacles/asset', createJsonHandler(
(req) => getAdminCloudtentaclesAsset(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 余额查询成功',
errorMessage: 'cloudtentacles 余额查询失败',
scope: '[admin/platform-config/cloudtentacles/asset]',
},
))
router.post('/platform-config/cloudtentacles/categories', createJsonHandler(
(req) => getAdminCloudtentaclesCategories(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 分类查询成功',
errorMessage: 'cloudtentacles 分类查询失败',
scope: '[admin/platform-config/cloudtentacles/categories]',
},
))
router.post('/platform-config/cloudtentacles/sku/list', createJsonHandler(
(req) => getAdminCloudtentaclesSkuList(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles SKU 列表查询成功',
errorMessage: 'cloudtentacles SKU 列表查询失败',
scope: '[admin/platform-config/cloudtentacles/sku/list]',
},
))
router.post('/platform-config/cloudtentacles/sku/buy', createJsonHandler(
(req) => buyAdminCloudtentaclesSku(/** @type {AdminCloudtentaclesSkuBuyRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles SKU 购买成功',
errorMessage: 'cloudtentacles SKU 购买失败',
scope: '[admin/platform-config/cloudtentacles/sku/buy]',
},
))
router.post('/platform-config/cloudtentacles/knapsack', createJsonHandler(
(req) => getAdminCloudtentaclesKnapsack(/** @type {AdminCloudtentaclesCatalogQueryRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 背包查询成功',
errorMessage: 'cloudtentacles 背包查询失败',
scope: '[admin/platform-config/cloudtentacles/knapsack]',
},
))
router.post('/platform-config/cloudtentacles/vn/list', createJsonHandler(
(req) => listAdminCloudtentaclesVirtualNumbers(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 虚拟号列表查询成功',
errorMessage: 'cloudtentacles 虚拟号列表查询失败',
scope: '[admin/platform-config/cloudtentacles/vn/list]',
},
))
router.post('/platform-config/cloudtentacles/vn/appoint', createJsonHandler(
(req) => appointAdminCloudtentaclesVirtualNumber(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 虚拟号申请成功',
errorMessage: 'cloudtentacles 虚拟号申请失败',
scope: '[admin/platform-config/cloudtentacles/vn/appoint]',
},
))
router.post('/platform-config/cloudtentacles/vn/generate-login-code', createJsonHandler(
(req) => generateAdminCloudtentaclesLoginCode(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 登录码生成成功',
errorMessage: 'cloudtentacles 登录码生成失败',
scope: '[admin/platform-config/cloudtentacles/vn/generate-login-code]',
},
))
router.post('/platform-config/cloudtentacles/vn/fetch-code', createJsonHandler(
(req) => fetchAdminCloudtentaclesVirtualNumberCode(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 验证码获取成功',
errorMessage: 'cloudtentacles 验证码获取失败',
scope: '[admin/platform-config/cloudtentacles/vn/fetch-code]',
},
))
router.post('/platform-config/cloudtentacles/vn/verify-code', createJsonHandler(
(req) => verifyAdminCloudtentaclesLoginCode(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 登录码校验成功',
errorMessage: 'cloudtentacles 登录码校验失败',
scope: '[admin/platform-config/cloudtentacles/vn/verify-code]',
},
))
router.post('/platform-config/cloudtentacles/vn/bind-url', createJsonHandler(
(req) => getAdminCloudtentaclesBindUrl(/** @type {AdminCloudtentaclesVirtualNumberRouteBody} */ (req.body)),
{
successMessage: 'cloudtentacles 兑换链接获取成功',
errorMessage: 'cloudtentacles 兑换链接获取失败',
scope: '[admin/platform-config/cloudtentacles/vn/bind-url]',
},
))
router.get('/platform-config/fulfillment-bindings', createJsonHandler(
() => getAdminFulfillmentBindingConfigs(),
{
@@ -20,11 +20,32 @@ import {
getCloudtentaclesSourcesFilePath,
saveCloudtentaclesSourceConfig,
} from '../platforms/cloudtentacles/source-config-service.js'
import {
clearCloudtentaclesSessionState,
getCloudtentaclesSessionFilePath,
getCloudtentaclesSessionState,
saveCloudtentaclesSessionState,
} from '../platforms/cloudtentacles/session-state-service.js'
import {
loginCloudtentaclesSession,
sendCloudtentaclesSmsCode,
validateCloudtentaclesSession,
} from '../platforms/cloudtentacles/session-service.js'
import {
buyCloudtentaclesSku,
getCloudtentaclesAsset,
getCloudtentaclesCategories,
listCloudtentaclesSku,
} from '../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../platforms/cloudtentacles/knapsack-service.js'
import {
appointCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode,
generateCloudtentaclesLoginCode,
getCloudtentaclesBindUrl,
listCloudtentaclesVirtualNumbers,
verifyCloudtentaclesLoginCode,
} from '../platforms/cloudtentacles/virtual-number-service.js'
import { normalizeAgisoMessageTemplate } from '../platforms/agiso/xianyu/message-service.js'
import {
getOrderFulfillmentBindingConfigs,
@@ -46,6 +67,9 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSendSmsCodeInput} AdminCloudtentaclesSendSmsCodeInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesTestLoginInput} AdminCloudtentaclesTestLoginInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesCatalogQueryInput} AdminCloudtentaclesCatalogQueryInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSkuBuyInput} AdminCloudtentaclesSkuBuyInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigSaveInput */
/** @typedef {import('../../types/admin-write-inputs.js').AdminFulfillmentBindingLookupInput} AdminFulfillmentBindingLookupInput */
@@ -352,9 +376,11 @@ export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQ
export function getAdminCloudtentaclesSourceConfig() {
const config = getCloudtentaclesSourceConfig()
const session = getCloudtentaclesSessionState()
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
source: {
enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || '').trim(),
@@ -364,11 +390,13 @@ export function getAdminCloudtentaclesSourceConfig() {
deviceId: String(config.deviceId || '-').trim() || '-',
deviceType: Number(config.deviceType || 0),
},
session: mapAdminCloudtentaclesSession(session),
}
}
/** @param {AdminCloudtentaclesSourceConfigInput} [payload] */
export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {AdminCloudtentaclesSourceConfigInput} */ ({})) {
const current = getCloudtentaclesSourceConfig()
const saved = saveCloudtentaclesSourceConfig({
enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
@@ -378,9 +406,12 @@ export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {Admin
deviceId: String(payload.deviceId || '-').trim() || '-',
deviceType: Number(payload.deviceType || 0),
})
const shouldClearSession = hasCloudtentaclesCredentialContextChanged(current, saved)
const session = shouldClearSession ? clearCloudtentaclesSessionState() : getCloudtentaclesSessionState()
return {
filePath: getCloudtentaclesSourcesFilePath(),
sessionFilePath: getCloudtentaclesSessionFilePath(),
source: {
enabled: saved.enabled !== false,
baseUrl: String(saved.baseUrl || '').trim(),
@@ -390,6 +421,7 @@ export function updateAdminCloudtentaclesSourceConfig(payload = /** @type {Admin
deviceId: String(saved.deviceId || '-').trim() || '-',
deviceType: Number(saved.deviceType || 0),
},
session: mapAdminCloudtentaclesSession(session),
}
}
@@ -422,6 +454,15 @@ export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminClo
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
})
const savedSession = saveCloudtentaclesSessionState({
token: session.token,
baseUrl: session.baseUrl,
username: session.username,
phone: session.phone,
loggedInAt: session.loggedInAt,
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
})
return {
baseUrl: session.baseUrl,
@@ -434,6 +475,7 @@ export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminClo
tokenMasked: maskSecret(session.token),
permissionCount: session.permissions.length,
permissions: session.permissions,
persisted: Boolean(savedSession.token),
},
userInfo: session.userInfo,
asset: session.asset,
@@ -443,12 +485,22 @@ export async function testAdminCloudtentaclesLogin(payload = /** @type {AdminClo
/** @param {AdminCloudtentaclesValidateSessionInput} [payload] */
export async function validateAdminCloudtentaclesSession(payload = /** @type {AdminCloudtentaclesValidateSessionInput} */ ({})) {
const savedSource = getCloudtentaclesSourceConfig()
const persistedSession = getCloudtentaclesSessionState()
const session = await validateCloudtentaclesSession({
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']),
token: String(payload.token || '').trim(),
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType,
})
const savedSession = saveCloudtentaclesSessionState({
token: session.token,
baseUrl: session.baseUrl,
username: pickFirstNonEmpty([persistedSession.username, savedSource.username]),
phone: pickFirstNonEmpty([persistedSession.phone, savedSource.phone]),
loggedInAt: session.loggedInAt,
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
})
return {
baseUrl: session.baseUrl,
@@ -457,12 +509,106 @@ export async function validateAdminCloudtentaclesSession(payload = /** @type {Ad
tokenMasked: maskSecret(session.token),
permissionCount: session.permissions.length,
permissions: session.permissions,
persisted: Boolean(savedSession.token),
},
userInfo: session.userInfo,
asset: session.asset,
}
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesAsset(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return getCloudtentaclesAsset(context)
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesCategories(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return getCloudtentaclesCategories(context)
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesSkuList(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return listCloudtentaclesSku(context)
}
/** @param {AdminCloudtentaclesSkuBuyInput} [payload] */
export async function buyAdminCloudtentaclesSku(payload = /** @type {AdminCloudtentaclesSkuBuyInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return buyCloudtentaclesSku({
...context,
id: payload.id,
count: payload.count,
})
}
/** @param {AdminCloudtentaclesCatalogQueryInput} [payload] */
export async function getAdminCloudtentaclesKnapsack(payload = /** @type {AdminCloudtentaclesCatalogQueryInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return getCloudtentaclesKnapsack(context)
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function listAdminCloudtentaclesVirtualNumbers(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return listCloudtentaclesVirtualNumbers({
...context,
key: payload.key,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function appointAdminCloudtentaclesVirtualNumber(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return appointCloudtentaclesVirtualNumber({
...context,
key: payload.key,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function generateAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return generateCloudtentaclesLoginCode({
...context,
key: payload.key,
id: payload.id,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function fetchAdminCloudtentaclesVirtualNumberCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return fetchCloudtentaclesVirtualNumberCode({
...context,
key: payload.key,
phone: payload.phone,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function verifyAdminCloudtentaclesLoginCode(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return verifyCloudtentaclesLoginCode({
...context,
key: payload.key,
id: payload.id,
code: payload.code,
})
}
/** @param {AdminCloudtentaclesVirtualNumberInput} [payload] */
export async function getAdminCloudtentaclesBindUrl(payload = /** @type {AdminCloudtentaclesVirtualNumberInput} */ ({})) {
const context = resolveCloudtentaclesAdminContext(payload)
return getCloudtentaclesBindUrl({
...context,
key: payload.key,
id: payload.id,
})
}
export async function getAdminFulfillmentBindingConfigs() {
const bindings = getOrderFulfillmentBindingConfigs()
const rowsResult = await query(
@@ -825,6 +971,42 @@ function maskPhone(value) {
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
}
function resolveCloudtentaclesAdminContext(payload = {}) {
const savedSource = getCloudtentaclesSourceConfig()
const persistedSession = getCloudtentaclesSessionState()
return {
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://123.207.217.176']),
token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId, '-']),
deviceType: payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
}
}
function hasCloudtentaclesCredentialContextChanged(current, next) {
return (
String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim()
|| String(current.username || '').trim() !== String(next.username || '').trim()
|| String(current.phone || '').trim() !== String(next.phone || '').trim()
|| String(current.deviceId || '').trim() !== String(next.deviceId || '').trim()
|| Number(current.deviceType || 0) !== Number(next.deviceType || 0)
)
}
function mapAdminCloudtentaclesSession(session = {}) {
return {
token: String(session.token || '').trim(),
tokenMasked: maskSecret(session.token),
baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: String(session.deviceId || '-').trim() || '-',
deviceType: Number(session.deviceType || 0),
hasToken: Boolean(String(session.token || '').trim()),
}
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,174 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './shared.js'
export async function getCloudtentaclesAsset(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.assetPath, {
...config,
method: 'GET',
token,
contentType: 'application/json',
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_asset_failed',
})
return {
baseUrl: config.baseUrl,
asset: Number(result.payload?.data || 0),
raw: result.payload?.data ?? null,
}
}
export async function getCloudtentaclesCategories(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 分类查询缺少 token', 'cloudtentacles_categories_missing_token')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.categoriesPath, {
...config,
method: 'GET',
token,
contentType: 'application/json',
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_categories_failed',
})
const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapCategoryItem) : []
return {
baseUrl: config.baseUrl,
itemCount: items.length,
items,
rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [],
}
}
export async function listCloudtentaclesSku(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles SKU 列表查询缺少 token', 'cloudtentacles_sku_list_missing_token')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.skuListPath, {
...config,
method: 'GET',
token,
contentType: 'application/json',
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_sku_list_failed',
})
const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapSkuItem) : []
return {
baseUrl: config.baseUrl,
itemCount: items.length,
items,
rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [],
}
}
export async function buyCloudtentaclesSku(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 购买 SKU 缺少 token', 'cloudtentacles_sku_buy_missing_token')
const skuId = requireId(payload.id, 'cloudtentacles 购买 SKU 缺少商品 id', 'cloudtentacles_sku_buy_missing_id')
const count = requireCount(payload.count)
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.skuBuyPath, {
...config,
method: 'POST',
token,
body: {
id: skuId,
count,
},
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_sku_buy_failed',
})
return {
baseUrl: config.baseUrl,
id: skuId,
count,
responseMessage: String(result.payload?.message || result.payload?.msg || 'success'),
}
}
function mapCategoryItem(item) {
const source = isPlainObject(item) ? item : {}
return {
id: Number(source.id || 0),
name: String(source.name || '').trim(),
supplierTag: String(source.supplier_tag || '').trim(),
description: String(source.description || '').trim(),
notes: String(source.notes || '').trim(),
expire: Number(source.expire || 0),
sendType: Number(source.send_type || 0),
exchangeUrl: String(source.exchange_url || '').trim(),
createdAt: String(source.c_time || '').trim(),
updatedAt: String(source.l_update_time || '').trim(),
raw: source,
}
}
function mapSkuItem(item) {
const source = isPlainObject(item) ? item : {}
return {
id: Number(source.id || 0),
categoriesId: Number(source.categories_id || 0),
name: String(source.name || '').trim(),
description: String(source.desc || '').trim(),
image: String(source.image || '').trim(),
inventory: Number(source.inventory || 0),
price: Number(source.price || 0),
listingTime: String(source.listing_time || '').trim(),
delistingTime: String(source.delisting_time || '').trim(),
buyLimitMin: Number(source.buy_limit_min || 0),
buyLimitMax: Number(source.buy_limit_max || 0),
raw: source,
}
}
function requireToken(value, message, errorCode) {
const token = String(value || '').trim()
if (!token) {
throw createHttpError(message, {
statusCode: 400,
errorCode,
})
}
return token
}
function requireId(value, message, errorCode) {
const id = Number(value)
if (!Number.isInteger(id) || id <= 0) {
throw createHttpError(message, {
statusCode: 400,
errorCode,
})
}
return id
}
function requireCount(value) {
const count = Number(value)
if (!Number.isInteger(count) || count <= 0) {
throw createHttpError('cloudtentacles 购买数量无效', {
statusCode: 400,
errorCode: 'cloudtentacles_sku_buy_invalid_count',
})
}
return count
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,53 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './shared.js'
export async function getCloudtentaclesKnapsack(payload = {}) {
const token = String(payload.token || '').trim()
if (!token) {
throw createHttpError('cloudtentacles 背包查询缺少 token', {
statusCode: 400,
errorCode: 'cloudtentacles_knapsack_missing_token',
})
}
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.knapsackPath, {
...config,
method: 'GET',
token,
contentType: 'application/json',
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_knapsack_failed',
})
const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapKnapsackItem) : []
return {
baseUrl: config.baseUrl,
itemCount: items.length,
items,
rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [],
}
}
function mapKnapsackItem(item) {
const source = isPlainObject(item) ? item : {}
return {
id: Number(source.id || 0),
name: String(source.name || '').trim(),
count: Number(source.count || 0),
image: String(source.image || '').trim(),
categoriesId: Number(source.categories_id || 0),
listingTime: String(source.listing_time || '').trim(),
delistingTime: String(source.delisting_time || '').trim(),
raw: source,
}
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,77 @@
// @ts-check
import fs from 'node:fs'
import path from 'node:path'
import { PROJECT_ROOT } from '../../../config/runtime.js'
const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json')
export function getCloudtentaclesSessionFilePath() {
return CLOUDTENTACLES_SESSION_FILE_PATH
}
export function getCloudtentaclesSessionState() {
return loadCloudtentaclesSessionStateFromFile()
}
export function saveCloudtentaclesSessionState(rawValue) {
const normalized = normalizeCloudtentaclesSessionState(rawValue)
fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true })
fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
return normalized
}
export function clearCloudtentaclesSessionState() {
const cleared = createDefaultCloudtentaclesSessionState()
saveCloudtentaclesSessionState(cleared)
return cleared
}
function loadCloudtentaclesSessionStateFromFile() {
if (!fs.existsSync(CLOUDTENTACLES_SESSION_FILE_PATH)) {
return createDefaultCloudtentaclesSessionState()
}
try {
const rawText = fs.readFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, 'utf8')
return normalizeCloudtentaclesSessionState(JSON.parse(rawText))
} catch {
return createDefaultCloudtentaclesSessionState()
}
}
function normalizeCloudtentaclesSessionState(rawValue) {
const source = isPlainObject(rawValue) ? rawValue : {}
return {
token: String(source.token || '').trim(),
baseUrl: String(source.baseUrl || '').trim(),
username: String(source.username || '').trim(),
phone: String(source.phone || '').trim(),
loggedInAt: String(source.loggedInAt || '').trim(),
deviceId: String(source.deviceId || '-').trim() || '-',
deviceType: normalizeInteger(source.deviceType, 0),
}
}
function createDefaultCloudtentaclesSessionState() {
return {
token: '',
baseUrl: '',
username: '',
phone: '',
loggedInAt: '',
deviceId: '-',
deviceType: 0,
}
}
function normalizeInteger(value, fallback) {
const parsed = Number(value)
return Number.isInteger(parsed) ? parsed : fallback
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -14,6 +14,16 @@ export function resolveCloudtentaclesConfig(overrides = {}) {
userInfoPath: '/user/info',
assetPath: '/user/get_asset',
permissionPath: '/user/get_permission',
categoriesPath: '/categories/get',
skuListPath: '/sku/list',
skuBuyPath: '/sku/buy',
knapsackPath: '/user/get_knapsack',
vnListPath: '/vn/list',
vnAppointPath: '/vn/appoint',
vnGenerateLoginCodePath: '/vn/generate_login_code',
vnVerifCodePath: '/public/vn_verif_code',
vnVerifyLoginCodePath: '/vn/verify_login_code',
vnBindUrlPath: '/vn/bind_url',
publicKeyPem: '',
clientSource: 'ct-client',
deviceId: '-',
@@ -28,6 +38,22 @@ export function resolveCloudtentaclesConfig(overrides = {}) {
userInfoPath: normalizePath(overrides.userInfoPath || baseConfig.userInfoPath, '/user/info'),
assetPath: normalizePath(overrides.assetPath || baseConfig.assetPath, '/user/get_asset'),
permissionPath: normalizePath(overrides.permissionPath || baseConfig.permissionPath, '/user/get_permission'),
categoriesPath: normalizePath(overrides.categoriesPath || baseConfig.categoriesPath, '/categories/get'),
skuListPath: normalizePath(overrides.skuListPath || baseConfig.skuListPath, '/sku/list'),
skuBuyPath: normalizePath(overrides.skuBuyPath || baseConfig.skuBuyPath, '/sku/buy'),
knapsackPath: normalizePath(overrides.knapsackPath || baseConfig.knapsackPath, '/user/get_knapsack'),
vnListPath: normalizePath(overrides.vnListPath || baseConfig.vnListPath, '/vn/list'),
vnAppointPath: normalizePath(overrides.vnAppointPath || baseConfig.vnAppointPath, '/vn/appoint'),
vnGenerateLoginCodePath: normalizePath(
overrides.vnGenerateLoginCodePath || baseConfig.vnGenerateLoginCodePath,
'/vn/generate_login_code',
),
vnVerifCodePath: normalizePath(overrides.vnVerifCodePath || baseConfig.vnVerifCodePath, '/public/vn_verif_code'),
vnVerifyLoginCodePath: normalizePath(
overrides.vnVerifyLoginCodePath || baseConfig.vnVerifyLoginCodePath,
'/vn/verify_login_code',
),
vnBindUrlPath: normalizePath(overrides.vnBindUrlPath || baseConfig.vnBindUrlPath, '/vn/bind_url'),
publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem),
clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client',
deviceId: String(overrides.deviceId ?? baseConfig.deviceId ?? '-').trim() || '-',
@@ -0,0 +1,222 @@
// @ts-check
import { createHttpError } from '../../../utils/http.js'
import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './shared.js'
export async function listCloudtentaclesVirtualNumbers(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 虚拟号列表缺少 token', 'cloudtentacles_vn_list_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 虚拟号列表缺少 key', 'cloudtentacles_vn_list_missing_key')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnListPath, {
...config,
method: 'POST',
token,
body: { key },
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_list_failed',
})
const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapVirtualNumberItem) : []
return {
baseUrl: config.baseUrl,
key,
itemCount: items.length,
items,
rawItems: Array.isArray(result.payload?.data) ? result.payload.data : [],
}
}
export async function appointCloudtentaclesVirtualNumber(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 申请虚拟号缺少 token', 'cloudtentacles_vn_appoint_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 申请虚拟号缺少 key', 'cloudtentacles_vn_appoint_missing_key')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnAppointPath, {
...config,
method: 'POST',
token,
body: { key },
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_appoint_failed',
})
return {
baseUrl: config.baseUrl,
key,
item: mapVirtualNumberItem(result.payload?.data),
rawItem: isPlainObject(result.payload?.data) ? result.payload.data : {},
}
}
export async function generateCloudtentaclesLoginCode(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 生成登录码缺少 token', 'cloudtentacles_vn_generate_code_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 生成登录码缺少 key', 'cloudtentacles_vn_generate_code_missing_key')
const id = requireId(payload.id, 'cloudtentacles 生成登录码缺少 id', 'cloudtentacles_vn_generate_code_missing_id')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnGenerateLoginCodePath, {
...config,
method: 'POST',
token,
body: { key, id },
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_generate_code_failed',
})
return {
baseUrl: config.baseUrl,
key,
id,
responseMessage: String(result.payload?.message || result.payload?.msg || 'success'),
}
}
export async function fetchCloudtentaclesVirtualNumberCode(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 获取验证码缺少 token', 'cloudtentacles_vn_verif_code_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 获取验证码缺少 key', 'cloudtentacles_vn_verif_code_missing_key')
const phone = String(payload.phone || '').trim()
if (!phone) {
throw createHttpError('cloudtentacles 获取验证码缺少手机号', {
statusCode: 400,
errorCode: 'cloudtentacles_vn_verif_code_missing_phone',
})
}
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnVerifCodePath, {
...config,
method: 'POST',
token,
body: { key, phone },
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_verif_code_failed',
})
return {
baseUrl: config.baseUrl,
key,
phone,
code: String(result.payload?.data || '').trim(),
responseMessage: String(result.payload?.message || result.payload?.msg || 'success'),
}
}
export async function verifyCloudtentaclesLoginCode(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 校验登录码缺少 token', 'cloudtentacles_vn_verify_code_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 校验登录码缺少 key', 'cloudtentacles_vn_verify_code_missing_key')
const id = requireId(payload.id, 'cloudtentacles 校验登录码缺少 id', 'cloudtentacles_vn_verify_code_missing_id')
const code = String(payload.code || '').trim()
if (!code) {
throw createHttpError('cloudtentacles 校验登录码缺少验证码', {
statusCode: 400,
errorCode: 'cloudtentacles_vn_verify_code_missing_code',
})
}
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnVerifyLoginCodePath, {
...config,
method: 'POST',
token,
body: { key, id, code },
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_verify_code_failed',
})
return {
baseUrl: config.baseUrl,
key,
id,
code,
responseMessage: String(result.payload?.message || result.payload?.msg || 'success'),
}
}
export async function getCloudtentaclesBindUrl(payload = {}) {
const token = requireToken(payload.token, 'cloudtentacles 获取兑换链接缺少 token', 'cloudtentacles_vn_bind_url_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 获取兑换链接缺少 key', 'cloudtentacles_vn_bind_url_missing_key')
const id = requireId(payload.id, 'cloudtentacles 获取兑换链接缺少 id', 'cloudtentacles_vn_bind_url_missing_id')
const config = resolveCloudtentaclesConfig(payload)
const result = await cloudtentaclesRequest(config.vnBindUrlPath, {
...config,
method: 'POST',
token,
body: { id, key },
businessErrorStatusCode: 401,
businessErrorCode: 'cloudtentacles_vn_bind_url_failed',
})
return {
baseUrl: config.baseUrl,
key,
id,
bindUrl: String(result.payload?.data || '').trim(),
responseMessage: String(result.payload?.message || result.payload?.msg || 'success'),
}
}
function mapVirtualNumberItem(item) {
const source = isPlainObject(item) ? item : {}
const bindInfoText = String(source.bind_info || '').trim()
return {
id: Number(source.id || 0),
phone: String(source.phone || '').trim(),
status: Number(source.status || 0),
countTime: Number(source.count_time || 0),
bindInfo: tryParseJson(bindInfoText) || bindInfoText || null,
raw: source,
}
}
function requireToken(value, message, errorCode) {
const token = String(value || '').trim()
if (!token) {
throw createHttpError(message, {
statusCode: 400,
errorCode,
})
}
return token
}
function requireKey(value, message, errorCode) {
const key = String(value || '').trim()
if (!key) {
throw createHttpError(message, {
statusCode: 400,
errorCode,
})
}
return key
}
function requireId(value, message, errorCode) {
const id = Number(value)
if (!Number.isInteger(id) || id <= 0) {
throw createHttpError(message, {
statusCode: 400,
errorCode,
})
}
return id
}
function tryParseJson(text) {
try {
return JSON.parse(text)
} catch {
return null
}
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -62,6 +62,18 @@ export {}
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesValidateSessionInput} AdminCloudtentaclesValidateSessionRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesCatalogQueryInput} AdminCloudtentaclesCatalogQueryRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesSkuBuyInput} AdminCloudtentaclesSkuBuyRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminCloudtentaclesVirtualNumberInput} AdminCloudtentaclesVirtualNumberRouteBody
*/
/**
* @typedef {import('./admin-write-inputs.js').AdminFulfillmentBindingConfigSaveInput} AdminFulfillmentBindingConfigRouteBody
*/
@@ -147,6 +147,39 @@ export {}
* }} AdminCloudtentaclesValidateSessionInput
*/
/**
* @typedef {{
* baseUrl?: string
* token?: string
* deviceId?: string
* deviceType?: number | string
* }} AdminCloudtentaclesCatalogQueryInput
*/
/**
* @typedef {{
* baseUrl?: string
* token?: string
* deviceId?: string
* deviceType?: number | string
* id?: number | string
* count?: number | string
* }} AdminCloudtentaclesSkuBuyInput
*/
/**
* @typedef {{
* baseUrl?: string
* token?: string
* deviceId?: string
* deviceType?: number | string
* key?: string
* id?: number | string
* phone?: string
* code?: string
* }} AdminCloudtentaclesVirtualNumberInput
*/
/**
* @typedef {{
* provider?: string
+10
View File
@@ -104,6 +104,16 @@ export {}
* userInfoPath: string
* assetPath: string
* permissionPath: string
* categoriesPath: string
* skuListPath: string
* skuBuyPath: string
* knapsackPath: string
* vnListPath: string
* vnAppointPath: string
* vnGenerateLoginCodePath: string
* vnVerifCodePath: string
* vnVerifyLoginCodePath: string
* vnBindUrlPath: string
* publicKeyPem: string
* clientSource: string
* deviceId: string