文件预览

lingxing.py

查看 LinkFoxAgent 技能包中的文件内容。

文件内容

scripts/lingxing.py

#!/usr/bin/env python3
"""
领星 OpenAPI CLI - 全模块查询接口

Usage:
    # 列出所有店铺(获取 SID)
    python3 lingxing.py --list-stores

    # 查询 SP 广告活动报表(SID 从环境变量自动注入)
    python3 lingxing.py --api spCampaignReports --params '{"report_date": "2024-01-01"}'

    # 自动翻页获取全部数据
    python3 lingxing.py --api spKeywordReports --params '{"report_date": "2024-01-01"}' --all

    # 查询产品列表
    python3 lingxing.py --api ProductLists --params '{"offset": 0, "length": 20}'

    # 查询利润报表
    python3 lingxing.py --api profitAsin --params '{"sids": "813", "start_date": "2024-01-01", "end_date": "2024-01-31"}'

    # 查询仓库列表
    python3 lingxing.py --api WarehouseLists --params '{"offset": 0, "length": 20}'

Environment:
    LINGXING_APP_ID     - 领星 AppID(必填)
    LINGXING_APP_SECRET - 领星 AppSecret(必填)
    LINGXING_SID        - 默认店铺 SID(选填;未在 --params 中指定 sid 时自动注入)
"""

import argparse
import base64
import hashlib
import json
import os
import sys
import time
import urllib.parse
from pathlib import Path
from typing import Optional

HOST = "https://openapi.lingxing.com"
TOKEN_CACHE_DIR = Path("/tmp")

# ─── API 路径映射 ────────────────────────────────────────────────────────────
# 默认路径: /pb/openapi/newad/<name>(仅适用于 newAd 系列接口)
# 所有非 newAd 接口必须在此注册

SPECIAL_PATHS = {
    # ── newAd 报表 - 特殊路径 ──
    "dspReportOrderList":           "/basicOpen/dspReport/order/list",
    "ProductAnalysisList":          "/basicOpen/adReport/productOrderAnalysis/list",
    # ── newAd 基础数据 - 特殊路径 ──
    "dspAccountList":               "/basicOpen/baseData/account/list",
    # ── newAd 报表下载 ──
    "abaReport":                    "/pb/openapi/newad/abaReport",

    # ── 基础数据 (BasicData) ──
    "AccoutLists":                  "/erp/sc/data/account/lists",
    "AllMarketplace":               "/erp/sc/data/seller/allMarketplace",
    "AttachmentDownload":           "/erp/sc/routing/common/file/download",
    "ConceptSellerLists":           "/erp/sc/data/seller/conceptLists",
    "Currency":                     "/erp/sc/routing/finance/currency/currencyMonth",
    "CustomAttachmentDownload":     "/erp/sc/routing/customized/file/download",
    "StateList":                    "/basicOpen/multiplatform/profit/report/stateList",
    "WorldStateLists":              "/erp/sc/data/worldState/lists",

    # ── 销售 (Sale) ──
    "mwsOrders":                    "/erp/sc/data/mws/orders",
    "mwsListing":                   "/erp/sc/data/mws/listing",
    "orderReturnInformation":       "/order/amzod/api/orderDetails/returnInformation",
    "scOrderSetRemark":             "/basicOpen/platformOrder/scOrder/setRemark",
    "OrderDetail":                  "/erp/sc/data/mws/orderDetail",
    "RefundOrder":                  "/basicOpen/openapi/salesOrder/refundOrder",
    "sale_ProductList":             "/listing/publish/openapi/amazon/product/list",
    "FBMOrderList":                 "/erp/sc/routing/order/Order/getOrderList",
    "FBMOrderDetail":               "/erp/sc/routing/order/Order/getOrderDetail",
    "MCFOrderList":                 "/order/amzod/api/orderList",
    "ProductInformation":           "/order/amzod/api/orderDetails/productInformation",
    "GetPrices":                    "/listing/listing/open/api/listing/getPrices",
    "LogisticsInformation":         "/order/amzod/api/orderDetails/logisticsInformation",
    "GetMerchantShippingGroup":     "/basicOpen/openapi/publish/manage/getMerchantShippingGroup",
    "GetFulfillmentResult":         "/pb/mp/order/getFulfillmentResult",
    "QueryProductList":             "/listing/publish/openapi/amazon/product/search",
    "PublishManageCategoryRoot":    "/basicOpen/openapi/publish/manage/categoryRoot",
    "PublishManageCategoryChildren": "/basicOpen/openapi/publish/manage/categoryChildren",
    "PublishManageGetProductType":  "/basicOpen/openapi/publish/manage/getProductType",
    "adjustPriceAdjustPriceManual": "/basicOpen/module/adjustPrice/AdjustPriceManual",
    "afterSaleList":                "/erp/sc/routing/amzod/order/afterSaleList",
    "fbaFeeDifferenceList":         "/basicOpen/openapi/sale/fbaFeeDifference/order/list",
    "fbaFeeDifferenceMskuList":     "/basicOpen/openapi/sale/fbaFeeDifference/msku/list",
    "globalTagPageList":            "/basicOpen/globalTag/listing/page/list",
    "listingOperateLogPageList":    "/basicOpen/listingManage/listingOperateLog/pageList",
    "promotionListingList":         "/basicOpen/promotion/listingList",
    "promotionListingDetailCoupon": "/basicOpen/promotion/listingDetailCoupon",
    "promotionListingDetailManage": "/basicOpen/promotion/listingDetailManage",
    "promotionListingDetailPrimeDiscount": "/basicOpen/promotion/listingDetailPrimeDiscount",
    "promotionListingDetailSecKill": "/basicOpen/promotion/listingDetailSecKill",
    "promotionalActivitiesCouponList": "/basicOpen/promotionalActivities/coupon/list",
    "promotionalActivitiesManageList": "/basicOpen/promotionalActivities/manage/list",
    "promotionalActivitiesSecKillList": "/basicOpen/promotionalActivities/secKill/list",
    "promotionalActivitiesVipDiscountList": "/basicOpen/promotionalActivities/vipDiscount/list",
    "queryListingRelationTagList":  "/basicOpen/listingManage/queryListingRelationTagList",
    "promotionCouponAllDetailBatch": "/promotionApi/open/promotion/couponAllDetailBatch",
    "promotionManagementAllDetailBatch": "/promotionApi/open/promotion/managementAllDetailBatch",
    "promotionPrimeDiscountAllDetailBatch": "/promotionApi/open/promotion/primeDiscountAllDetailBatch",
    "promotionSecKillAllDetailBatch": "/promotionApi/open/promotion/secKillAllDetailBatch",

    # ── 产品 (Product) ──
    "ProductLists":                 "/erp/sc/routing/data/local_inventory/productList",
    "ProductDetails":               "/erp/sc/routing/data/local_inventory/productInfo",
    "Brand":                        "/erp/sc/data/local_inventory/brand",
    "Category":                     "/erp/sc/routing/data/local_inventory/category",
    "UpcList":                      "/listing/publish/api/upc/upcList",
    "GetProductTag":                "/label/operation/v1/label/product/list",
    "GetPagingLogLists":            "/basicOpen/product/getPagingLogLists",
    "attributeList":                "/erp/sc/routing/storage/attribute/attributeList",
    "batchGetProductInfo":          "/erp/sc/routing/data/local_inventory/batchGetProductInfo",
    "bundledProductList":           "/erp/sc/routing/data/local_inventory/bundledProductList",
    "getTransparencyProductList":   "/basicOpen/product/getTransparencyProductList",
    "productAuxList":               "/erp/sc/routing/data/local_inventory/productAuxList",
    "spuInfo":                      "/erp/sc/routing/storage/spu/info",
    "spuList":                      "/erp/sc/routing/storage/spu/spuList",

    # ── 财务 (Finance) ──
    "profitAsin":                   "/erp/sc/routing/finance/ProfitState/profitAsin",
    "profitAsinSon":                "/erp/sc/routing/finance/ProfitState/profitAsinSon",
    "profitSettlement":             "/erp/sc/routing/finance/ProfitState/profitSettlement",
    "FianceProfitMsku":             "/erp/sc/routing/finance/ProfitState/profitMsku",
    "SettlementReport":             "/cost/center/api/settlement/report",
    "CostStream":                   "/cost/center/api/cost/stream",
    "OrderProfitListMSKU":          "/basicOpen/finance/mreport/OrderProfit",
    "InvoiceList":                  "/bd/profit/report/open/report/ads/invoice/list",
    "InvoiceDetail":                "/bd/profit/report/open/report/ads/invoice/detail",
    "InvoiceCampaignList":          "/bd/profit/report/open/report/ads/invoice/campaign/list",
    "QueryReceiptFundsList":        "/basicOpen/finance/queryReceiptFundsList",
    "RequestFundsOrderList":        "/basicOpen/finance/requestFunds/order/list",
    "SettlementExportUrlGet":       "/bd/sp/api/open/settlement/export/url/get",
    "bdASIN":                       "/bd/profit/report/open/report/asin/list",
    "bdMSKU":                       "/bd/profit/report/open/report/msku/list",
    "bdOrder":                      "/bd/profit/report/open/report/order/list",
    "bdParentASIN":                 "/bd/profit/report/open/report/parent/asin/list",
    "bdSKU":                        "/bd/profit/report/open/report/sku/list",
    "bdSeller":                     "/bd/profit/report/open/report/seller/list",
    "bdSellerSummary":              "/bd/profit/report/open/report/seller/summary/list",
    "centerOdsDetailQuery":         "/cost/center/ods/detail/query",
    "feeManagementList":            "/bd/fee/management/open/feeManagement/otherFee/list",
    "feeManagementType":            "/bd/fee/management/open/feeManagement/otherFee/type",
    "lazadaPayoutList":             "/basicOpen/finance/lazada/payout/list",
    "lazadaSettlementList":         "/basicOpen/finance/lazada/settlement/list",
    "profitReportOrderTranscationList": "/basicOpen/finance/profitReport/order/transcation/list",
    "receivableReportList":         "/bd/sp/api/open/monthly/receivable/report/list",
    "reportListDetail":             "/bd/sp/api/open/monthly/receivable/report/list/detail",
    "reportListDetailInfo":         "/bd/sp/api/open/monthly/receivable/report/list/detail/info",
    "requestFundsPoolCustomFeeList": "/basicOpen/finance/requestFundsPool/customFee/list",
    "requestFundsPoolInboundList":  "/basicOpen/finance/requestFundsPool/inbound/list",
    "requestFundsPoolLogisticsList": "/basicOpen/finance/requestFundsPool/logistics/list",
    "requestFundsPoolOtherFeeList": "/basicOpen/finance/requestFundsPool/otherFee/list",
    "requestFundsPoolPrepayList":   "/basicOpen/finance/requestFundsPool/prepay/list",
    "requestFundsPoolPurchaseList": "/basicOpen/finance/requestFundsPool/purchase/list",
    "settlementSummaryList":        "/bd/sp/api/open/settlement/summary/list",
    "settlementTransactionList":    "/bd/sp/api/open/settlement/transaction/detail/list",
    "shopeeAdjustmentList":         "/basicOpen/finance/shopee/adjustment/list",
    "shopeeIncomeList":             "/basicOpen/finance/shopee/income/list",
    "shopeePayoutList":             "/basicOpen/finance/shopee/payout/list",
    "summaryQuery":                 "/cost/center/ods/summary/query",

    # ── 统计 (Statistics) ──
    "AsinDailyLists":               "/erp/sc/data/sales_report/asinDailyLists",
    "AsinList":                     "/erp/sc/data/sales_report/asinList",
    "AsinListNew":                  "/bd/productPerformance/openApi/asinList",
    "StoreSales":                   "/erp/sc/data/sales_report/sales",
    "FBAStorageFeeLongTerm":        "/erp/sc/data/fba_report/storageFeeLongTerm",
    "FBAStorageFeeMonth":           "/erp/sc/data/fba_report/storageFeeMonth",
    "FbaStockAggregateListNew":     "/cost/center/openApi/fba/gather/query",
    "FbaStockDetailListNew":        "/cost/center/openApi/fba/detail/query",
    "FbaStockReportList":           "/erp/sc/routing/fba/fbaStockReport/getList",
    "LocalAggregateList":           "/erp/sc/routing/inventoryLog/WareHouseReport/getLocalWareHouseSummaryList",
    "LocalAggregateListNew":        "/inventory/center/openapi/storageReport/local/aggregate/list",
    "LocalDetailList":              "/erp/sc/routing/inventoryLog/WareHouseReport/getLocalWareHouseDetailList",
    "LocalDetailListNew":           "/inventory/center/openapi/storageReport/local/detail/page",
    "MonthRefund":                  "/erp/sc/routing/finance/Refund/profitMonthRefund",
    "OverseasAggregateList":        "/erp/sc/routing/inventoryLog/WareHouseReport/getOverSeaSummaryList",
    "OverseasAggregateListNew":     "/inventory/center/openapi/storageReport/overseas/aggregate/list",
    "OverseasDetailList":           "/erp/sc/routing/inventoryLog/WareHouseReport/getOverSeaDetailList",
    "OverseasDetailListNew":        "/inventory/center/openapi/storageReport/overseas/detail/page",
    "PlatformStatisticsSaleStatPageListV2": "/basicOpen/platformStatisticsV2/saleStat/pageList",
    "ProfitMsku":                   "/erp/sc/routing/finance/ProfitStatis/profitMsku",
    "PurchaseReportBuyerList":      "/basicOpen/report/purchase/buyer/list",
    "PurchaseReportProductList":    "/basicOpen/report/purchase/product/list",
    "PurchaseReportSupplierList":   "/basicOpen/report/purchase/supplier/list",
    "ReimbursementList":            "/basicOpen/openapi/mwsReport/reimbursementList",
    "ReturnOrderAnalysisLists":     "/basicOpen/salesAnalysis/returnOrder/analysisLists",
    "operateLogList":               "/basicOpen/operateManage/operateLog/list",
    "operateLogV2List":             "/basicOpen/operateManage/operateLog/list/v2",
    "performanceTrendByHour":       "/basicOpen/salesAnalysis/productPerformance/performanceTrendByHour",
    "AmazonReportExportTask":       "/basicOpen/report/amazonReportExportTask",
    "reportCreateReportExportTask": "/basicOpen/report/create/reportExportTask",
    "reportQueryReportExportTask":  "/basicOpen/report/query/reportExportTask",
    "statisticsOpenASIN":           "/bd/profit/statistics/open/asin/list",
    "statisticsOpenMSKU":           "/bd/profit/statistics/open/msku/list",
    "statisticsOpenParent":         "/bd/profit/statistics/open/parent/asin/list",
    "statisticsOpenSeller":         "/bd/profit/statistics/open/seller/list",

    # ── FBA发货 ──
    "FBAShipmentList":              "/erp/sc/data/fba_report/shipmentList",
    "FBAReceivedInventory":         "/erp/sc/data/fba_report/receivedInventory",
    "ShipmentPlanLists":            "/erp/sc/data/fba_report/shipmentPlanLists",
    "GetFbaProductList":            "/erp/sc/routing/fba/shipment/getFbaProductList",
    "GetInboundShipmentList":       "/erp/sc/routing/storage/shipment/getInboundShipmentList",
    "GetInboundShipmentListMwsDetailList": "/erp/sc/routing/storage/shipment/getInboundShipmentListMwsDetailList",
    "ShipFromAddressList":          "/erp/sc/routing/fba/shipment/shipFromAddressList",
    "BoxInfo":                      "/erp/sc/routing/fba/shipment/boxInfo",
    "GetHeadLogisticsFeeTypes":     "/erp/sc/routing/fba/shipment/getHeadLogisticsFeeTypes",
    "GetSeaTrackSupplierCarriers":  "/erp/sc/routing/fba/shipment/getSeaTrackSupplierCarriers",
    "QuerySTATaskList":             "/amzStaServer/openapi/inbound-plan/page",
    "StaTaskDetail":                "/amzStaServer/openapi/inbound-plan/detail",
    "getInboundShipmentListMwsDetail": "/erp/sc/routing/storage/shipment/getInboundShipmentListMwsDetail",

    # ── 亚马逊源表数据 (SourceData) ──
    "AllOrders":                    "/erp/sc/data/mws_report/allOrders",
    "FbaOrders":                    "/erp/sc/data/mws_report/fbaOrders",
    "RefundOrders":                 "/erp/sc/data/mws_report/refundOrders",
    "Transaction":                  "/erp/sc/data/mws_report/transaction",
    "ManageInventory":              "/erp/sc/data/mws_report/manageInventory",
    "DailyInventory":               "/erp/sc/data/mws_report/dailyInventory",
    "AfnFulfillableQuantity":       "/erp/sc/data/mws_report/getAfnFulfillableQuantity",
    "ReservedInventory":            "/erp/sc/data/mws_report/reservedInventory",
    "RemovalLists":                 "/erp/sc/data/fba_report/removalLists",
    "RemovalOrderListNew":          "/erp/sc/routing/data/order/removalOrderListNew",
    "RemovalShipmentList":          "/erp/sc/statistic/removalShipment/list",
    "SourceRemovalOrders":          "/erp/sc/data/mws_report/removalOrders",
    "AdjustmentList":               "/basicOpen/openapi/mwsReport/adjustmentList",
    "getAmazonFulfilledShipmentsList": "/erp/sc/data/mws_report/getAmazonFulfilledShipmentsList",
    "getFbaAgeList":                "/erp/sc/routing/fba/fbaStock/getFbaAgeList",
    "getFbaInventoryEventDetailList": "/erp/sc/data/mws_report/getFbaInventoryEventDetailList",
    "v1getAmazonFulfilledShipmentsList": "/erp/sc/data/mws_report_v1/getAmazonFulfilledShipmentsList",
    "v1getFbaInventoryEventDetailList": "/erp/sc/data/mws_report_v1/getFbaInventoryEventDetailList",
    "fbaExchangeOrderList":         "/erp/sc/routing/data/order/fbaExchangeOrderList",
    "fbmReturnOrderList":           "/erp/sc/routing/data/order/fbmReturnOrderList",

    # ── 补货建议 (FBASug) ──
    "GetSummaryList":               "/erp/sc/routing/restocking/analysis/getSummaryList",
    "ConfigASIN":                   "/erp/sc/routing/fbaSug/asin/getConfig",
    "ConfigMSKU":                   "/erp/sc/routing/fbaSug/msku/getConfig",
    "DailySalesInfoFeatureASIN":    "/erp/sc/routing/fbaSug/asin/getDailySalesInfoFeature",
    "DailySalesInfoFeatureMSKU":    "/erp/sc/routing/fbaSug/msku/getDailySalesInfoFeature",
    "InfoASIN":                     "/erp/sc/routing/fbaSug/asin/getInfo",
    "InfoMSKU":                     "/erp/sc/routing/fbaSug/msku/getInfo",
    "SourceListASIN":               "/erp/sc/routing/fbaSug/asin/getSourceList",
    "SourceListMSKU":               "/erp/sc/routing/fbaSug/msku/getSourceList",

    # ── 补货限制 (FBALimit) ──
    "GetIpiInfo":                   "/erp/sc/routing/fbaLimit/restock/getIpiInfo",
    "replenishmentRestrictionList": "/basicOpen/openapi/replenishmentRestriction/page/list",

    # ── 采购 (Purchase) ──
    "PurchaseOrderList":            "/erp/sc/routing/data/local_inventory/purchaseOrderList",
    "Supplier":                     "/erp/sc/data/local_inventory/supplier",
    "changeOrderList":              "/erp/sc/routing/purchase/purchaseChangeOrder/changeOrderList",
    "purchaseGetOrders":            "/erp/sc/routing/purchase/purchaseOutsourceOrder/getOrders",
    "getPurchasePlans":             "/erp/sc/routing/data/local_inventory/getPurchasePlans",
    "getPurchaseReturnOrderList":   "/erp/sc/routing/purchase/purchase_return_order/getPurchaseReturnOrderList",
    "purchaserLists":               "/erp/sc/routing/data/purchaser/lists",

    # ── 仓库 (Warehouse) ──
    "WarehouseLists":               "/erp/sc/data/local_inventory/warehouse",
    "WarehouseStatement":           "/erp/sc/routing/data/local_inventory/wareHouseStatement",
    "WarehouseStatementNew":        "/erp/sc/routing/inventoryLog/WareHouseInventory/wareHouseCenterStatement",
    "InventoryDetails":             "/erp/sc/routing/data/local_inventory/inventoryDetails",
    "inventoryBinDetails":          "/erp/sc/routing/data/local_inventory/inventoryBinDetails",
    "wareHouseBinStatement":        "/erp/sc/routing/data/local_inventory/wareHouseBinStatement",
    "warehouseBin":                 "/erp/sc/routing/data/local_inventory/warehouseBin",
    "FBAStock":                     "/erp/sc/routing/fba/fbaStock/fbaList",
    "FBAStock_v2":                  "/basicOpen/openapi/storage/fbaWarehouseDetail",
    "AwdWarehouseDetail":           "/basicOpen/openapi/storage/awdWarehouseDetail",
    "GetBatchDetailList":           "/erp/sc/routing/data/local_inventory/getBatchDetailList",
    "GetBatchStatementList":        "/erp/sc/routing/data/local_inventory/getBatchStatementList",
    "GetReceiveGoodRecords":        "/erp/sc/routing/owms/inbound/getReceiveGoodRecords",
    "PurchaseReceiptOrderList":     "/erp/sc/routing/deliveryReceipt/PurchaseReceiptOrder/getOrderList",
    "ReceiptOrderQcList":           "/erp/sc/routing/deliveryReceipt/ReceiptOrderQc/getOrderList",
    "WmsOrderList":                 "/erp/sc/routing/wms/order/wmsOrderList",
    "WmsOrderDetail":               "/basicOpen/wmsOrder/getWmsOrdersByOrderNumbers",
    "OverseaWarehouseMatchList":    "/basicOpen/overseaWarehouseSetting/matchList",
    "OverSeasStockDetail":          "/basicOpen/overSeaWarehouse/stockOrder/detail",
    "getProcessOrderLists":         "/erp/sc/routing/inventoryReceipt/StorageProcess/getOrderLists",
    "getStorageAdjustOrderList":    "/erp/sc/routing/inventoryReceipt/StorageAdjustment/getStorageAdjustOrderList",
    "getStorageAllocationList":     "/erp/sc/routing/inventoryReceipt/StorageAllocation/getStorageAllocationList",
    "ReturnList":                   "/pb/mp/returns/v2/list",
    "inboundgetOrders":             "/erp/sc/routing/storage/inbound/getOrders",
    "outboundgetOrders":            "/erp/sc/routing/storage/outbound/getOrders",
    "removalInboundList":           "/erp/sc/routing/owms/removalInbound/list",
    "matchSkuList":                 "/erp/sc/routing/owms/inbound/matchSkuList",
    "listInbound":                  "/erp/sc/routing/owms/inbound/listInbound",
    "checkGetOrderList":            "/erp/sc/routing/inventoryReceipt/InventoryCheck/getOrderList",
    "checkGetOrderDetail":          "/erp/sc/routing/inventoryReceipt/InventoryCheck/getOrderDetail",
    "getPackingData":               "/erp/sc/routing/owms/inbound/getPackingData",
    "qualityInspectionOrderDetail": "/basicOpen/qualityInspectionOrder/detail",

    # ── 客服 (Service) ──
    "FeedbackList":                 "/erp/sc/cs/feedback/list",
    "FeedbackListMws":              "/erp/sc/cs/feedback/listMws",
    "CustomerList":                 "/bd/crm/open/api/customer/list",
    "AfterSalesWorkOrderList":      "/pb/mp/returns/workOrder/list",
    "PerformanceNoticeList":        "/basicOpen/customerService/performanceNotice/list",
    "PerformanceNoticeDetail":      "/basicOpen/customerService/storeTarget/detail",
    "mailDetail":                   "/erp/sc/data/mail/detail",
    "mailLists":                    "/erp/sc/data/mail/lists",
    "review":                       "/erp/sc/v2/data/mws/reviews",
    "reviewDetail":                 "/erp/sc/cs/reviewReport/detail",
    "reviewLists":                  "/erp/sc/v2/cs/reviewReport/lists",
    "reviewV2":                     "/basicOpen/openapi/service/v3/data/mws/reviews",
    "feedbackDetail":               "/erp/sc/cs/feedbackReport/detail",
    "feedbackLists":                "/erp/sc/cs/feedbackReport/lists",
    "customerServiceCrmcustomerIndex": "/basicOpen/customerService/crm/customer/index",
    "customerServiceRmaManageList": "/basicOpen/customerService/rmaManage/list",
    "storePerformanceList":         "/basicOpen/customerService/storeTarget/list",
    "voiceOfBuyerList":             "/basicOpen/customerService/voiceOfBuyer/list",

    # ── 物流 (Logistics) ──
    "ChannelList":                  "/erp/sc/data/local_inventory/channelList",
    "QueryHeadLogisticsProvider":   "/basicOpen/logistics/headLogisticsProvider/query/list",
    "transportMethodList":          "/basicOpen/businessConfig/transportMethod/list",

    # ── 工具 (Tools) ──
    "CompetitiveMonitorList":       "/basicOpen/tool/competitiveMonitor/list",
    "GetKeywordList":               "/erp/sc/routing/tool/toolKeywordRank/getKeywordList",
    "warningMessageGoodsList":      "/basicOpen/settings/warningMessage/goodsList",
    "warningMessageInventoryList":  "/basicOpen/settings/warningMessage/inventoryList",

    # ── VC ──
    "vcOrderPageList":              "/basicOpen/platformOrder/vcOrder/pageList",
    "vcOrderPoDetail":              "/basicOpen/platformOrder/vcOrderPo/detail",
    "vcOrderDfDetail":              "/basicOpen/platformOrder/vcOrderDf/detail",
    "vcDeliverPageList":            "/basicOpen/openapi/getInvoice/page/list",
    "vcDeliverDetail":              "/basicOpen/openapi/getInvoice/detail",
    "listingManageVcListingPageList": "/basicOpen/listingManage/vcListing/pageList",
    "platformAuthVcSellerPageList": "/basicOpen/platformAuth/vcSeller/pageList",

    # ── 目标管理 (TargetManage) ──
    "StoreBatchSelect":             "/bd/goal/management/open/store/batchSelect",
    "UserBatchSelect":              "/bd/goal/management/open/user/batchSelect",

    # ── 多平台广告 (MultiPlatform/Advertisement) ──
    "queryCommonAdvertiserList":    "/basicOpen/multiplatform/ads/queryCommonAdvertiserList",
    "queryGmvStoreList":            "/basicOpen/multiplatform/ads/queryGmvStoreList",
    "queryTiktokAdGroupList":       "/basicOpen/multiplatform/ads/queryTiktokAdGroupList",
    "queryTiktokAdList":            "/basicOpen/multiplatform/ads/queryTiktokAdList",
    "queryAdvertiserList":          "/basicOpen/multiplatform/ads/queryAdvertiserList",
    "queryTiktokCampaignList":      "/basicOpen/multiplatform/ads/queryTiktokCampaignList",
    "queryGmvAdvertiserReportList": "/basicOpen/multiplatform/ads/queryGmvAdvertiserReportList",
    "queryGmvCampaignReportList":   "/basicOpen/multiplatform/ads/queryGmvCampaignReportList",
    "queryGmvItemGroupReportList":  "/basicOpen/multiplatform/ads/queryGmvItemGroupReportList",
    "queryAdGroupSvList":           "/basicOpen/multiplatform/ads/queryAdGroupSvList",
    "queryCampaignSpList":          "/basicOpen/multiplatform/ads/queryCampaignSpList",
    "queryGroupSpList":             "/basicOpen/multiplatform/ads/queryGroupSpList",
    "queryPageTypeSPList":          "/basicOpen/multiplatform/ads/queryPageTypeSPList",
    "queryReportPageTypeSvList":    "/basicOpen/multiplatform/ads/queryReportPageTypeSvList",
    "reportAdGroupSbList":          "/basicOpen/multiplatform/ads/reportAdGroupSbList",
    "reportAdItemSbList":           "/basicOpen/multiplatform/ads/reportAdItemSbList",
    "reportAdItemSpList":           "/basicOpen/multiplatform/ads/reportAdItemSpList",
    "reportAdItemSvList":           "/basicOpen/multiplatform/ads/reportAdItemSvList",
    "reportCampaignSbList":         "/basicOpen/multiplatform/ads/reportCampaignSbList",
    "reportCampaignSvList":         "/basicOpen/multiplatform/ads/reportCampaignSvList",
    "reportKeywordSbList":          "/basicOpen/multiplatform/ads/reportKeywordSbList",
    "reportKeywordSpList":          "/basicOpen/multiplatform/ads/reportKeywordSpList",
    "reportKeywordSvList":          "/basicOpen/multiplatform/ads/reportKeywordSvList",
    "reportPageTypeSbList":         "/basicOpen/multiplatform/ads/reportPageTypeSbList",
    "reportPlatformSbList":         "/basicOpen/multiplatform/ads/reportPlatformSbList",
    "reportPlatformSpList":         "/basicOpen/multiplatform/ads/reportPlatformSpList",
    "reportPlatformSvList":         "/basicOpen/multiplatform/ads/reportPlatformSvList",
    "reportSearchTrendsList":       "/basicOpen/multiplatform/ads/reportSearchTrendsList",

    # ── 多平台V2 (MultiPlatform/V2) - 查询类 ──
    "MultiPlatOrderV2":             "/pb/mp/order/v2/list",
    "StoreInfoV2":                  "/pb/mp/shop/v2/getSellerList",
    "PairListV2":                   "/pb/mp/listing/v2/getPairList",
    "QueryShippingListV2":          "/basicOpen/multiplatform/query/shippingList",
    "QueryShippingListPage":        "/cepf/warehouse/api/openApi/queryShippingListPage",
    "QueryWFSCargoPage":            "/cepf/warehouse/api/openApi/queryWFSCargoPage",
    "QueryWFSInventionPage":        "/cepf/warehouse/api/openApi/queryWFSInventionPage",
    "AliexpressListV2":             "/basicOpen/multiplatform/aliexpress/list/v2",
    "aliexpressList":               "/basicOpen/multiplatform/aliExpress/list",
    "eBayList":                     "/basicOpen/multiplatform/ebay/list",
    "walmartList":                  "/basicOpen/multiplatform/walmart/list",
    "TemuList":                     "/basicOpen/multiplatform/temu/list",
    "TemuCargo":                    "/basicOpen/multiplatform/temu/cargo",
    "TikTokList":                   "/basicOpen/multiplatform/tiktok/list",
    "SheinList":                    "/basicOpen/multiplatform/shein/list",
    "ShopifyVariantList":           "/basicOpen/multiplatform/shopify/variantList",
    "CoupangStockList":             "/basicOpen/multiplatform/coupang/stockSearch",
    "FbsStockList":                 "/basicOpen/multiplatform/fbs/stockSearch",
    "FbtStockList":                 "/basicOpen/multiplatform/fbt/stockSearch/v2",
    "FbtStockSearch":               "/basicOpen/multiplatform/fbt/stockSearch",
    "FullList":                     "/basicOpen/multiplatform/full/stockSearch",
    "WayfairStockList":             "/basicOpen/multiplatform/wayfair/stockSearch",
    "LineList":                     "/basicOpen/multiplatform/line/list",
    "addCargoGoodsList":            "/basicOpen/multiplatform/cargo/addCargoGoods/list",
    "WalmartPaymentQueryPage":      "/cepf/fms/openapi/walmartPayment/queryPage",
    "WalmartPaymentQueryReport":    "/cepf/fms/openapi/walmartPayment/queryReport",
    "WalmartCommentList":           "/basicOpen/multiplatform/walmart/queryCommentList",
    "profitReportMsku":             "/basicOpen/multiplatform/profit/report/msku",
    "profitReportOrder":            "/basicOpen/multiplatform/profit/report/order",
    "profitReportSeller":           "/basicOpen/multiplatform/profit/report/seller",
    "profitReportSku":              "/basicOpen/multiplatform/profit/report/sku",
    "newPlatformOrderList":         "/cepfPlatformOrder/open-api/newPlatformOrder/list",
    "addressReturnAddressList":     "/basicOpen/multiplatform/address/returnAddressList",
}

# ── 需要 GET 方法的接口(默认为 POST)──
GET_APIS = {
    "AccoutLists", "AllMarketplace", "ConceptSellerLists",
    "GetProductTag",
    "reviewLists",
    "getPackingData",
}

# ── 使用 page/page_size 分页的接口(默认用 offset/length)──
# 值为 (page_key, size_key)
PAGE_APIS = {
    "WmsOrderList":              ("page", "page_size"),
    "checkGetOrderList":         ("page", "page_size"),
    "checkGetOrderDetail":       ("page", "page_size"),
    "getStorageAdjustOrderList": ("page", "page_size"),
    "getStorageAllocationList":  ("page", "page_size"),
    "listInbound":               ("page", "page_size"),
    "FBMOrderList":              ("page", "length"),
    "QuerySTATaskList":          ("page", "length"),
    "PlatformStatisticsSaleStatPageListV2": ("page", "length"),
    "GetPagingLogLists":         ("page", "size"),
}

# ── 响应格式特殊的接口 ──
# cost center API 返回 data.records 或 data.row_data,而非直接 data
COST_CENTER_APIS = {
    "SettlementReport", "CostStream",
    "centerOdsDetailQuery", "summaryQuery",
    "FbaStockAggregateListNew", "FbaStockDetailListNew",
}

SUPPORTED_APIS = [
    # ── newAd 报表 (37个) ──
    "spCampaignReports", "campaignPlacementReports", "spAdGroupReports",
    "spProductAdReports", "spKeywordReports", "spTargetReports",
    "asinReports", "queryWordReports",
    "hsaCampaignReports", "hsaCampaignPlacementReports", "hsaAdGroupReports",
    "listHsaTargetingReport", "hsaQueryWordReports", "hsaPurchasedAsinReports",
    "listHsaProductAdReport", "listHsaKeywordPlacementReport",
    "sdCampaignReports", "sdAdGroupReports", "sdProductAdReports",
    "sdTargetReports", "sdAsinReports", "sdMatchTargetReports",
    "spCampaignHourData", "spAdPlacementHourData", "spAdGroupHourData",
    "spAdvertiseHourData", "spTargetHourData",
    "sbCampaignHourData", "sbAdGroupHourData", "sbTargetHourData", "sbAdPlacementHourData",
    "sdCampaignHourData", "sdAdGroupHourData", "sdAdvertiseHourData", "sdTargetHourData",
    "dspReportOrderList", "ProductAnalysisList",
    # ── newAd 基础数据 (20个) ──
    "dspAccountList", "portfolios",
    "spCampaigns", "spAdGroups", "spProductAds", "spKeywords", "spTargets",
    "spNegativeTargetsOrKeywords",
    "hsaCampaigns", "hsaAdGroups", "hsaProductAds", "sbTargeting",
    "hsaNegativeKeywords", "hsaNegativeTargets", "sbDivideAsinReports",
    "sdCampaigns", "sdAdGroups", "sdProductAds", "sdTargets", "sdNegativeTargets",
    # ── newAd 报表下载 ──
    "abaReport",
    # ── 基础数据 ──
    "AccoutLists", "AllMarketplace", "AttachmentDownload", "ConceptSellerLists",
    "Currency", "CustomAttachmentDownload", "StateList", "WorldStateLists",
    # ── 销售 ──
    "mwsOrders", "mwsListing", "orderReturnInformation", "scOrderSetRemark",
    "OrderDetail", "RefundOrder", "sale_ProductList", "FBMOrderList", "FBMOrderDetail",
    "MCFOrderList", "ProductInformation", "GetPrices", "LogisticsInformation",
    "GetMerchantShippingGroup", "GetFulfillmentResult", "QueryProductList",
    "PublishManageCategoryRoot", "PublishManageCategoryChildren", "PublishManageGetProductType",
    "adjustPriceAdjustPriceManual", "afterSaleList",
    "fbaFeeDifferenceList", "fbaFeeDifferenceMskuList",
    "globalTagPageList", "listingOperateLogPageList",
    "promotionListingList", "promotionListingDetailCoupon", "promotionListingDetailManage",
    "promotionListingDetailPrimeDiscount", "promotionListingDetailSecKill",
    "promotionalActivitiesCouponList", "promotionalActivitiesManageList",
    "promotionalActivitiesSecKillList", "promotionalActivitiesVipDiscountList",
    "queryListingRelationTagList",
    "promotionCouponAllDetailBatch", "promotionManagementAllDetailBatch",
    "promotionPrimeDiscountAllDetailBatch", "promotionSecKillAllDetailBatch",
    # ── 产品 ──
    "ProductLists", "ProductDetails", "Brand", "Category", "UpcList",
    "GetProductTag", "GetPagingLogLists",
    "attributeList", "batchGetProductInfo", "bundledProductList",
    "getTransparencyProductList", "productAuxList",
    "spuInfo", "spuList",
    # ── 财务 ──
    "profitAsin", "profitAsinSon", "profitSettlement", "FianceProfitMsku",
    "SettlementReport", "CostStream", "OrderProfitListMSKU",
    "InvoiceList", "InvoiceDetail", "InvoiceCampaignList",
    "QueryReceiptFundsList", "RequestFundsOrderList", "SettlementExportUrlGet",
    "bdASIN", "bdMSKU", "bdOrder", "bdParentASIN", "bdSKU", "bdSeller", "bdSellerSummary",
    "centerOdsDetailQuery", "feeManagementList", "feeManagementType",
    "lazadaPayoutList", "lazadaSettlementList",
    "profitReportOrderTranscationList",
    "receivableReportList", "reportListDetail", "reportListDetailInfo",
    "requestFundsPoolCustomFeeList", "requestFundsPoolInboundList",
    "requestFundsPoolLogisticsList", "requestFundsPoolOtherFeeList",
    "requestFundsPoolPrepayList", "requestFundsPoolPurchaseList",
    "settlementSummaryList", "settlementTransactionList",
    "shopeeAdjustmentList", "shopeeIncomeList", "shopeePayoutList",
    "summaryQuery",
    # ── 统计 ──
    "AsinDailyLists", "AsinList", "AsinListNew", "StoreSales",
    "FBAStorageFeeLongTerm", "FBAStorageFeeMonth",
    "FbaStockAggregateListNew", "FbaStockDetailListNew", "FbaStockReportList",
    "LocalAggregateList", "LocalAggregateListNew",
    "LocalDetailList", "LocalDetailListNew",
    "MonthRefund",
    "OverseasAggregateList", "OverseasAggregateListNew",
    "OverseasDetailList", "OverseasDetailListNew",
    "PlatformStatisticsSaleStatPageListV2", "ProfitMsku",
    "PurchaseReportBuyerList", "PurchaseReportProductList", "PurchaseReportSupplierList",
    "ReimbursementList", "ReturnOrderAnalysisLists",
    "operateLogList", "operateLogV2List", "performanceTrendByHour",
    "AmazonReportExportTask", "reportCreateReportExportTask", "reportQueryReportExportTask",
    "statisticsOpenASIN", "statisticsOpenMSKU", "statisticsOpenParent", "statisticsOpenSeller",
    # ── FBA发货 ──
    "FBAShipmentList", "FBAReceivedInventory", "ShipmentPlanLists",
    "GetFbaProductList", "GetInboundShipmentList", "GetInboundShipmentListMwsDetailList",
    "ShipFromAddressList", "BoxInfo", "GetHeadLogisticsFeeTypes",
    "GetSeaTrackSupplierCarriers", "QuerySTATaskList", "StaTaskDetail",
    "getInboundShipmentListMwsDetail",
    # ── 亚马逊源表数据 ──
    "AllOrders", "FbaOrders", "RefundOrders", "Transaction",
    "ManageInventory", "DailyInventory", "AfnFulfillableQuantity", "ReservedInventory",
    "RemovalLists", "RemovalOrderListNew", "RemovalShipmentList", "SourceRemovalOrders",
    "AdjustmentList", "getAmazonFulfilledShipmentsList",
    "getFbaAgeList", "getFbaInventoryEventDetailList",
    "v1getAmazonFulfilledShipmentsList", "v1getFbaInventoryEventDetailList",
    "fbaExchangeOrderList", "fbmReturnOrderList",
    # ── 补货建议 ──
    "GetSummaryList", "ConfigASIN", "ConfigMSKU",
    "DailySalesInfoFeatureASIN", "DailySalesInfoFeatureMSKU",
    "InfoASIN", "InfoMSKU", "SourceListASIN", "SourceListMSKU",
    # ── 补货限制 ──
    "GetIpiInfo", "replenishmentRestrictionList",
    # ── 采购 ──
    "PurchaseOrderList", "Supplier", "changeOrderList",
    "purchaseGetOrders", "getPurchasePlans", "getPurchaseReturnOrderList", "purchaserLists",
    # ── 仓库 ──
    "WarehouseLists", "WarehouseStatement", "WarehouseStatementNew",
    "InventoryDetails", "inventoryBinDetails", "wareHouseBinStatement", "warehouseBin",
    "FBAStock", "FBAStock_v2", "AwdWarehouseDetail",
    "GetBatchDetailList", "GetBatchStatementList", "GetReceiveGoodRecords",
    "PurchaseReceiptOrderList", "ReceiptOrderQcList",
    "WmsOrderList", "WmsOrderDetail",
    "OverseaWarehouseMatchList", "OverSeasStockDetail",
    "getProcessOrderLists", "getStorageAdjustOrderList", "getStorageAllocationList",
    "ReturnList", "inboundgetOrders", "outboundgetOrders",
    "removalInboundList", "matchSkuList", "listInbound",
    "checkGetOrderList", "checkGetOrderDetail", "getPackingData",
    "qualityInspectionOrderDetail",
    # ── 客服 ──
    "FeedbackList", "FeedbackListMws", "CustomerList", "AfterSalesWorkOrderList",
    "PerformanceNoticeList", "PerformanceNoticeDetail",
    "mailDetail", "mailLists", "review", "reviewDetail", "reviewLists", "reviewV2",
    "feedbackDetail", "feedbackLists",
    "customerServiceCrmcustomerIndex", "customerServiceRmaManageList",
    "storePerformanceList", "voiceOfBuyerList",
    # ── 物流 ──
    "ChannelList", "QueryHeadLogisticsProvider", "transportMethodList",
    # ── 工具 ──
    "CompetitiveMonitorList", "GetKeywordList",
    "warningMessageGoodsList", "warningMessageInventoryList",
    # ── VC ──
    "vcOrderPageList", "vcOrderPoDetail", "vcOrderDfDetail",
    "vcDeliverPageList", "vcDeliverDetail",
    "listingManageVcListingPageList", "platformAuthVcSellerPageList",
    # ── 目标管理 ──
    "StoreBatchSelect", "UserBatchSelect",
    # ── 多平台广告 ──
    "queryCommonAdvertiserList", "queryGmvStoreList",
    "queryTiktokAdGroupList", "queryTiktokAdList",
    "queryAdvertiserList", "queryTiktokCampaignList",
    "queryGmvAdvertiserReportList", "queryGmvCampaignReportList",
    "queryGmvItemGroupReportList",
    "queryAdGroupSvList", "queryCampaignSpList", "queryGroupSpList",
    "queryPageTypeSPList", "queryReportPageTypeSvList",
    "reportAdGroupSbList", "reportAdItemSbList", "reportAdItemSpList", "reportAdItemSvList",
    "reportCampaignSbList", "reportCampaignSvList",
    "reportKeywordSbList", "reportKeywordSpList", "reportKeywordSvList",
    "reportPageTypeSbList",
    "reportPlatformSbList", "reportPlatformSpList", "reportPlatformSvList",
    "reportSearchTrendsList",
    # ── 多平台V2 ──
    "MultiPlatOrderV2", "StoreInfoV2", "PairListV2",
    "QueryShippingListV2", "QueryShippingListPage",
    "QueryWFSCargoPage", "QueryWFSInventionPage",
    "AliexpressListV2", "aliexpressList", "eBayList", "walmartList",
    "TemuList", "TemuCargo", "TikTokList", "SheinList",
    "ShopifyVariantList", "CoupangStockList",
    "FbsStockList", "FbtStockList", "FbtStockSearch", "FullList",
    "WayfairStockList", "LineList", "addCargoGoodsList",
    "WalmartPaymentQueryPage", "WalmartPaymentQueryReport",
    "WalmartCommentList",
    "profitReportMsku", "profitReportOrder", "profitReportSeller", "profitReportSku",
    "newPlatformOrderList", "addressReturnAddressList",
]


# ─── HTTP 客户端(requests 优先,不可用时降级 urllib)────────────────────────

def _http_post(url: str, params: Optional[dict] = None,
               json_body: Optional[dict] = None,
               form_body: Optional[dict] = None,
               headers: Optional[dict] = None,
               timeout: int = 60) -> dict:
    req_headers = dict(headers or {})
    try:
        import requests as _req
        if form_body:
            resp = _req.post(url, params=params, data=form_body,
                             headers=req_headers, timeout=timeout)
        else:
            resp = _req.post(url, params=params, json=json_body,
                             headers=req_headers, timeout=timeout)
        resp.raise_for_status()
        return resp.json()
    except ImportError:
        pass  # 降级到 urllib

    # urllib 降级路径
    import urllib.request, urllib.error
    if params:
        url = url + "?" + urllib.parse.urlencode(params)
    if form_body:
        data = urllib.parse.urlencode(form_body).encode()
        req_headers["Content-Type"] = "application/x-www-form-urlencoded"
    elif json_body is not None:
        data = json.dumps(json_body, ensure_ascii=False, sort_keys=True).encode()
        req_headers.setdefault("Content-Type", "application/json")
    else:
        data = b""
    req = urllib.request.Request(url, data=data, headers=req_headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e


def _http_get(url: str, params: Optional[dict] = None,
              headers: Optional[dict] = None, timeout: int = 60) -> dict:
    req_headers = dict(headers or {})
    try:
        import requests as _req
        resp = _req.get(url, params=params, headers=req_headers, timeout=timeout)
        resp.raise_for_status()
        return resp.json()
    except ImportError:
        pass

    import urllib.request, urllib.error
    if params:
        url = url + "?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, headers=req_headers)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e


# ─── AES / 签名 ──────────────────────────────────────────────────────────────

def _aes_encrypt(key: str, data: str) -> str:
    try:
        from Crypto.Cipher import AES
    except ImportError:
        print("Error: pycryptodome 未安装。请运行:pip install pycryptodome", file=sys.stderr)
        sys.exit(1)
    BLOCK = 16
    pad_len = BLOCK - len(data) % BLOCK
    padded = data + chr(pad_len) * pad_len
    return base64.b64encode(
        AES.new(key.encode(), AES.MODE_ECB).encrypt(padded.encode())
    ).decode()


def _generate_sign(app_id: str, params: dict) -> str:
    """领星签名:ASCII排序 → MD5大写 → AES-ECB-Base64"""
    parts = []
    for k in sorted(params.keys()):
        v = params[k]
        if v == "":
            continue
        if isinstance(v, (dict, list)):
            v_str = json.dumps(v, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
        else:
            v_str = str(v)
        parts.append(f"{k}={v_str}")
    md5_upper = hashlib.md5("&".join(parts).encode()).hexdigest().upper()
    return _aes_encrypt(app_id, md5_upper)


def _sign_params(app_id: str, token: str, extra: Optional[dict] = None) -> dict:
    """构造公共签名参数(含 sign)。extra 是需要参与签名的业务参数。"""
    base = {
        "app_key":      app_id,
        "access_token": token,
        "timestamp":    str(int(time.time())),
    }
    sign_src = dict(extra or {})
    sign_src.update(base)
    base["sign"] = urllib.parse.quote(_generate_sign(app_id, sign_src), safe="")
    return base


# ─── Token 缓存 ───────────────────────────────────────────────────────────────

def _token_cache_path(app_id: str) -> Path:
    return TOKEN_CACHE_DIR / f"lingxing_token_{app_id[:8]}.json"


def _load_cached_token(app_id: str) -> Optional[str]:
    path = _token_cache_path(app_id)
    if not path.exists():
        return None
    try:
        data = json.loads(path.read_text())
        if data.get("expires_at", 0) > time.time() + 60:
            return data["access_token"]
    except Exception:
        pass
    return None


def _save_token(app_id: str, token: str, expires_in: int) -> None:
    path = _token_cache_path(app_id)
    path.write_text(json.dumps({"access_token": token, "expires_at": time.time() + expires_in}))


def get_access_token(app_id: str, app_secret: str) -> str:
    """获取 access_token,自动使用本地缓存(2小时有效)。"""
    cached = _load_cached_token(app_id)
    if cached:
        return cached
    resp = _http_post(
        f"{HOST}/api/auth-server/oauth/access-token",
        form_body={"appId": app_id, "appSecret": app_secret},
    )
    if str(resp.get("code")) != "200":
        raise RuntimeError(f"获取 token 失败: {resp}")
    data = resp["data"]
    _save_token(app_id, data["access_token"], int(data["expires_in"]))
    return data["access_token"]


# ─── 店铺列表 ─────────────────────────────────────────────────────────────────

def list_seller_stores(app_id: str, token: str) -> list:
    """
    GET /erp/sc/data/seller/lists
    返回 list[dict],每个 dict 含 sid / name / seller_id / country / has_ads_setting / status
    """
    qp = _sign_params(app_id, token)
    resp = _http_get(
        f"{HOST}/erp/sc/data/seller/lists",
        params=qp,
        headers={"X-API-VERSION": "2"},
    )
    code = resp.get("code")
    if code == "403" or code == 403:
        raise PermissionError(
            "403 权限不足。SellerLists 接口需要 ERP 全量权限,"
            "当前 AppKey 可能仅授权了广告数据。\n"
            "解决方法:在领星 ERP → 开放接口 中为该 AppKey 添加「基础数据」权限,"
            "或在环境变量中直接设置 LINGXING_SID=<数字> 跳过自动发现。"
        )
    if code not in (0, "0"):
        raise RuntimeError(f"SellerLists 返回错误: {resp}")
    return resp.get("data") or []


def print_stores_table(stores: list) -> None:
    """打印店铺列表表格"""
    print(f"\n{'SID':>6}  {'店铺名':20}  {'Amazon Seller ID':18}  {'国家':6}  {'广告授权':6}  {'状态'}")
    print("-" * 75)
    for s in stores:
        ads = "✓" if s.get("has_ads_setting") == 1 else "✗"
        status_map = {0: "停止", 1: "正常", 2: "授权异常", 3: "欠费"}
        status = status_map.get(s.get("status"), str(s.get("status")))
        print(f"{s.get('sid',''):>6}  {str(s.get('name',''))[:20]:20}  "
              f"{s.get('seller_id',''):18}  {s.get('country',''):6}  "
              f"{ads:6}  {status}")
    print()
    print(f"提示:将常用 SID 设置为环境变量后无需每次在 --params 中传入:")
    print(f"  export LINGXING_SID=<sid>")


# ─── API 调用 ────────────────────────────────────────────────────────────────

def get_api_path(api_name: str) -> str:
    return SPECIAL_PATHS.get(api_name, f"/pb/openapi/newad/{api_name}")


def _extract_data(resp: dict, api_name: str) -> tuple:
    """从响应中提取 data 和 total,兼容不同响应格式。返回 (data_list, total)"""
    raw_data = resp.get("data")

    # cost center API: data 是 dict,实际数据在 records/row_data 下
    if api_name in COST_CENTER_APIS and isinstance(raw_data, dict):
        records = raw_data.get("records") or raw_data.get("row_data") or []
        total = raw_data.get("total") or resp.get("total") or 0
        return records, total

    # 标准格式
    data = raw_data if isinstance(raw_data, list) else []
    total = resp.get("total") or 0
    return data, total


def _check_response_ok(resp: dict, api_name: str) -> None:
    """检查响应状态码是否表示成功"""
    code = resp.get("code")
    # 标准格式: code=0; cost center / targetmanage: code=1
    if code in (0, "0", 1, "1"):
        return
    # 有些接口用 success=true
    if resp.get("success") is True:
        return
    raise RuntimeError(
        f"API 错误: code={code} msg={resp.get('message') or resp.get('msg')} "
        f"detail={resp.get('error_details')}"
    )


def call_api(app_id: str, token: str, api_name: str,
             body: dict, extra_headers: Optional[dict] = None) -> dict:
    """单次调用(含签名),自动判断 GET/POST"""
    is_get = api_name in GET_APIS
    qp = _sign_params(app_id, token, extra=None if is_get else body)
    headers = {"X-API-VERSION": "2"}
    if extra_headers:
        headers.update(extra_headers)

    url = HOST + get_api_path(api_name)

    if is_get:
        # GET: body 参数放到 query params
        qp.update(body)
        return _http_get(url, params=qp, headers=headers)
    else:
        return _http_post(url, params=qp, json_body=body, headers=headers)


def call_api_all(app_id: str, token: str, api_name: str,
                 body: dict, page_size: int = 100) -> list:
    """自动分页,拉取全部数据。支持 offset/length 和 page/page_size 两种模式。"""
    all_data = []

    if api_name in PAGE_APIS:
        # page/page_size 模式
        pk, sk = PAGE_APIS[api_name]
        page = body.get(pk, 1)
        while True:
            page_body = {**body, pk: page, sk: page_size}
            resp = call_api(app_id, token, api_name, page_body)
            _check_response_ok(resp, api_name)
            data, total = _extract_data(resp, api_name)
            all_data.extend(data)
            print(f"[分页] page={page} 获取{len(data)}条,累计{len(all_data)}/{total}", file=sys.stderr)
            if not data or len(all_data) >= total:
                break
            page += 1
    else:
        # offset/length 模式(默认)
        offset = body.get("offset", 0)
        while True:
            page_body = {**body, "offset": offset, "length": page_size}
            resp = call_api(app_id, token, api_name, page_body)
            _check_response_ok(resp, api_name)
            data, total = _extract_data(resp, api_name)
            all_data.extend(data)
            offset += len(data)
            print(f"[分页] {offset}/{total}", file=sys.stderr)
            if not data or offset >= total:
                break

    return all_data


# ─── 主程序 ───────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="领星 OpenAPI CLI - 全模块查询",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--api",
                       help=f"接口名。支持 {len(SUPPORTED_APIS)} 个接口,"
                            f"用 --api help 查看完整列表")
    group.add_argument("--list-stores", action="store_true",
                       help="列出所有已授权亚马逊店铺及其 SID(需 ERP 全量权限)")

    parser.add_argument("--params", default="{}",
                        help="请求参数 JSON。若 LINGXING_SID 已设置且未指定 sid,则自动注入。")
    parser.add_argument("--all", dest="fetch_all", action="store_true",
                        help="自动翻页获取全部数据")
    parser.add_argument("--page-size", type=int, default=100,
                        help="--all 模式每页条数(默认 100)")
    args = parser.parse_args()

    # ── help 模式 ──
    if args.api == "help":
        print("支持的接口列表:")
        for name in SUPPORTED_APIS:
            path = get_api_path(name)
            method = "GET" if name in GET_APIS else "POST"
            print(f"  {name:45s} {method:4s} {path}")
        return

    # ── 凭证 ──
    app_id     = os.environ.get("LINGXING_APP_ID")
    app_secret = os.environ.get("LINGXING_APP_SECRET")
    if not app_id or not app_secret:
        print("Error: 请设置环境变量:\n"
              "  export LINGXING_APP_ID=your_app_id\n"
              "  export LINGXING_APP_SECRET=your_app_secret",
              file=sys.stderr)
        sys.exit(1)

    # ── Token ──
    try:
        token = get_access_token(app_id, app_secret)
    except Exception as e:
        print(f"Error: 获取 access_token 失败: {e}", file=sys.stderr)
        sys.exit(1)

    # ── --list-stores ──
    if args.list_stores:
        try:
            stores = list_seller_stores(app_id, token)
            print_stores_table(stores)
        except PermissionError as e:
            print(f"Warning: {e}", file=sys.stderr)
            sys.exit(1)
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            sys.exit(1)
        return

    # ── --api ──
    if args.api not in SUPPORTED_APIS:
        print(f"Error: 不支持的接口 '{args.api}'", file=sys.stderr)
        print(f"共 {len(SUPPORTED_APIS)} 个接口,用 --api help 查看完整列表", file=sys.stderr)
        sys.exit(1)

    try:
        body = json.loads(args.params)
    except json.JSONDecodeError as e:
        print(f"Error: --params 不是合法 JSON: {e}", file=sys.stderr)
        sys.exit(1)

    # 自动注入 SID
    env_sid = os.environ.get("LINGXING_SID")
    if env_sid and "sid" not in body and "profile_id" not in body:
        try:
            body["sid"] = int(env_sid)
            print(f"[info] 已从环境变量注入 sid={body['sid']}", file=sys.stderr)
        except ValueError:
            print(f"Warning: LINGXING_SID='{env_sid}' 不是整数,已忽略", file=sys.stderr)

    try:
        if args.fetch_all:
            data = call_api_all(app_id, token, args.api, body, args.page_size)
            result = {"total": len(data), "data": data}
        else:
            result = call_api(app_id, token, args.api, body)
    except Exception as e:
        print(f"Error: API 调用失败: {e}", file=sys.stderr)
        sys.exit(1)

    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()