import type { JsonObject } from '../../types/json.js' import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js' import { getCloudtentaclesSessionStateByKey } from '../platforms/cloudtentacles/session-state-service.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, } from '../platforms/cloudtentacles/defaults.js' import { listCloudtentaclesSku } from '../platforms/cloudtentacles/catalog-service.js' import { resolveCloudtentaclesOverrideRule, type CloudtentaclesOverrideRule, } from './cloudtentacles-override-rule-service.js' import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js' type CloudtentaclesSource = { key?: string label?: string enabled?: boolean baseUrl?: string deviceId?: string deviceType?: number } type CloudtentaclesSession = { token?: string baseUrl?: string deviceId?: string deviceType?: number } type CloudtentaclesSku = { id?: number name?: string inventory?: number price?: number [key: string]: unknown } type CloudtentaclesNameMatchDeps = { listCloudtentaclesSources?: typeof listCloudtentaclesSources getCloudtentaclesSessionStateByKey?: typeof getCloudtentaclesSessionStateByKey listCloudtentaclesSku?: typeof listCloudtentaclesSku resolveCloudtentaclesOverrideRule?: typeof resolveCloudtentaclesOverrideRule } export type CloudtentaclesResolvedDeliveryItem = { cloudSkuId: number cloudSkuName: string quantity: number } export type CloudtentaclesNameMatchResult = { matchMode: 'cloudtentacles_name' | 'cloudtentacles_override' productName: string normalizedProductName: string cloudSkuId: number cloudSkuName: string cloudSkuPrice: number cloudSkuInventory: number cloudSourceKeys: string[] resolvedSourceKey: string deliveryItems: CloudtentaclesResolvedDeliveryItem[] skuSnapshot: JsonObject } export async function resolveCloudtentaclesSkuByProductName( productName: unknown, deps: CloudtentaclesNameMatchDeps = {}, ): Promise { const normalizedProductName = normalizeCloudtentaclesMatchName(productName) if (!normalizedProductName) { return null } const listSources = deps.listCloudtentaclesSources || listCloudtentaclesSources const getSessionByKey = deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey const listSku = deps.listCloudtentaclesSku || listCloudtentaclesSku const resolveOverrideRule = deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule const sourcesConfig = listSources() if (sourcesConfig.enabled === false) { return null } const sourceContexts = (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []) .map((source: CloudtentaclesSource) => buildCloudtentaclesSourceContext(source, getSessionByKey), ) .filter( (context): context is NonNullable> => Boolean(context), ) if (sourceContexts.length === 0) { return null } const overrideRule = resolveOverrideRule(productName) const overrideMatch = overrideRule ? await resolveOverrideMatch( productName, normalizedProductName, overrideRule, sourceContexts, listSku, ) : null if (overrideMatch) { return overrideMatch } const cloudSourceKeys = sourceContexts.map((context) => context.sourceKey) for (const context of sourceContexts) { try { const skuList = await listSku(context) const matchedSku = findCloudtentaclesSkuByName( Array.isArray(skuList.items) ? skuList.items : [], productName, ) if (!matchedSku) { continue } const cloudSkuId = Number(matchedSku.id || 0) const cloudSkuName = String(matchedSku.name || '').trim() if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0 || !cloudSkuName) { continue } return { matchMode: 'cloudtentacles_name', productName: String(productName || '').trim(), normalizedProductName, cloudSkuId, cloudSkuName, cloudSkuPrice: Number(matchedSku.price || 0) || 0, cloudSkuInventory: Number(matchedSku.inventory || 0) || 0, cloudSourceKeys, // Matching only discovers compatible accounts. The fulfillment selector // chooses the least-loaded account immediately before taking a number. resolvedSourceKey: '', deliveryItems: [ { cloudSkuId, cloudSkuName, quantity: 1, }, ], skuSnapshot: { ...matchedSku }, } } catch { // 当前账号不可用时继续尝试下一个已登录账号。 } } return null } function buildCloudtentaclesSourceContext( source: CloudtentaclesSource, getSessionByKey: typeof getCloudtentaclesSessionStateByKey, ) { const sourceKey = String(source.key || '').trim() if (!sourceKey || source.enabled === false) { return null } const session = getSessionByKey(sourceKey) as CloudtentaclesSession | null const token = String(session?.token || '').trim() if (!token) { return null } return { sourceKey, accountLabel: String(source.label || sourceKey).trim() || sourceKey, baseUrl: String(session?.baseUrl || source.baseUrl || '').trim(), token, deviceId: normalizeCloudtentaclesDeviceId(session?.deviceId || source.deviceId), deviceType: normalizeCloudtentaclesDeviceType(session?.deviceType ?? source.deviceType), } } function findCloudtentaclesSkuByName(items: CloudtentaclesSku[], productName: unknown) { const rawName = String(productName || '').trim() const normalizedName = normalizeCloudtentaclesMatchName(rawName) return ( items.find((item) => String(item.name || '').trim() === rawName) || items.find((item) => normalizeCloudtentaclesMatchName(item.name) === normalizedName) || null ) } async function resolveOverrideMatch( productName: unknown, normalizedProductName: string, rule: CloudtentaclesOverrideRule, sourceContexts: Array>>, listSku: typeof listCloudtentaclesSku, ): Promise { const candidates = rule.sourceKey ? sourceContexts.filter((context) => context.sourceKey === rule.sourceKey) : sourceContexts if (candidates.length === 0) { return null } for (const context of candidates) { try { const skuList = await listSku(context) const items = Array.isArray(skuList.items) ? skuList.items : [] const deliveryItems = resolveOverrideDeliveryItems(rule, items) if (deliveryItems.length !== rule.deliveryItems.length || !deliveryItems[0]) { continue } const firstDeliveryItem = deliveryItems[0] const firstSku = items.find((item) => Number(item.id || 0) === firstDeliveryItem.cloudSkuId) || {} const cloudSourceKeys = rule.sourceKey ? [context.sourceKey] : sourceContexts.map((sourceContext) => sourceContext.sourceKey) return { matchMode: 'cloudtentacles_override', productName: String(productName || '').trim(), normalizedProductName, cloudSkuId: firstDeliveryItem.cloudSkuId, cloudSkuName: firstDeliveryItem.cloudSkuName, cloudSkuPrice: Number(firstSku.price || 0) || 0, cloudSkuInventory: Number(firstSku.inventory || 0) || 0, cloudSourceKeys, resolvedSourceKey: rule.sourceKey ? context.sourceKey : '', deliveryItems, skuSnapshot: { overrideRuleId: rule.id, overrideRuleProductName: rule.productName, deliveryItems, firstSku, }, } } catch { // 当前账号不可用时继续尝试下一个已登录账号。 } } return null } function resolveOverrideDeliveryItems( rule: CloudtentaclesOverrideRule, skuItems: CloudtentaclesSku[], ) { const resolvedItems: CloudtentaclesResolvedDeliveryItem[] = [] for (const ruleItem of rule.deliveryItems) { const matchedSku = ruleItem.cloudSkuId > 0 ? skuItems.find((item) => Number(item.id || 0) === ruleItem.cloudSkuId) : findCloudtentaclesSkuByName(skuItems, ruleItem.cloudSkuName) const cloudSkuId = Number(matchedSku?.id || 0) const cloudSkuName = String(matchedSku?.name || '').trim() if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0 || !cloudSkuName) { return [] } resolvedItems.push({ cloudSkuId, cloudSkuName, quantity: Math.max(1, Number(ruleItem.quantity || 1)), }) } return mergeResolvedDeliveryItems(resolvedItems) } function mergeResolvedDeliveryItems(items: CloudtentaclesResolvedDeliveryItem[]) { const merged = new Map() for (const item of items) { const existing = merged.get(item.cloudSkuId) if (existing) { existing.quantity += item.quantity existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName continue } merged.set(item.cloudSkuId, { ...item }) } return Array.from(merged.values()) } export { normalizeCloudtentaclesMatchName }