v1.6: Platform Gateway — Amazon/JD/Taobao/PDD/TikTok 5平台API集成 + start_provision_v2.sh
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Synchronous facade for the Gateway library.
|
||||
|
||||
``fetch`` is the entry point used by ``chatwoot_ws_agent.py``. It runs
|
||||
on the main thread but schedules its work onto ``gateway_loop`` and blocks
|
||||
on the result. All caching, breaker, and rate-limit logic lives here so
|
||||
the adapters stay minimal.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from . import amazon, jd, taobao, pdd, tiktok
|
||||
from .base import UnifiedResult
|
||||
from .breaker import get_breaker, get_tenant_limiter
|
||||
from .cache import TTLCache
|
||||
from .credentials import load_credentials
|
||||
from .loop import gateway_loop
|
||||
|
||||
log = logging.getLogger("chathub.gateway.router")
|
||||
|
||||
_cache = TTLCache(ttl_seconds=60, max_size=1000)
|
||||
_HANDLERS = {
|
||||
"amazon": amazon.fetch,
|
||||
"jd": jd.fetch,
|
||||
"taobao": taobao.fetch,
|
||||
"pdd": pdd.fetch,
|
||||
"tiktok": tiktok.fetch,
|
||||
}
|
||||
|
||||
|
||||
def _query_hash(channel: str, tenant_id: int, query: dict) -> str:
|
||||
raw = json.dumps({"c": channel, "t": tenant_id, "q": query}, sort_keys=True)
|
||||
return hashlib.sha256(raw.encode()).hexdigest()
|
||||
|
||||
|
||||
async def _call_channel(channel: str, creds: dict, query: dict) -> UnifiedResult:
|
||||
handler = _HANDLERS.get(channel)
|
||||
if not handler:
|
||||
return UnifiedResult(status="error", error=f"unknown channel: {channel}", channel=channel)
|
||||
breaker = get_breaker(channel)
|
||||
if not breaker.allow():
|
||||
return UnifiedResult(status="breaker_open", error="circuit breaker open", channel=channel)
|
||||
try:
|
||||
result = await handler(creds, query)
|
||||
if result.ok:
|
||||
breaker.on_success()
|
||||
else:
|
||||
breaker.on_failure()
|
||||
return result
|
||||
except Exception as e:
|
||||
breaker.on_failure()
|
||||
return UnifiedResult(status="error", error=str(e)[:200], channel=channel)
|
||||
|
||||
|
||||
async def _async_fetch(channel: str, tenant_id: int, query: dict, timeout: float) -> UnifiedResult:
|
||||
cache_key = f"{channel}:{tenant_id}:{json.dumps(query, sort_keys=True)}"
|
||||
cached = _cache.get(cache_key)
|
||||
if cached is not None:
|
||||
# mark as cache_hit
|
||||
hit = UnifiedResult(
|
||||
status="cache_hit",
|
||||
data=cached.data,
|
||||
latency_ms=0,
|
||||
channel=channel,
|
||||
)
|
||||
return hit
|
||||
creds = load_credentials(tenant_id, channel)
|
||||
if not creds:
|
||||
return UnifiedResult(status="no_creds", error="channel not configured", channel=channel)
|
||||
limiter = get_tenant_limiter(tenant_id)
|
||||
await limiter.acquire(str(tenant_id))
|
||||
start = time.time()
|
||||
try:
|
||||
result = await asyncio.wait_for(_call_channel(channel, creds, query), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
result = UnifiedResult(status="timeout", error=f"timed out after {timeout}s", channel=channel)
|
||||
result.latency_ms = int((time.time() - start) * 1000)
|
||||
if result.ok:
|
||||
_cache.set(cache_key, result)
|
||||
return result
|
||||
|
||||
|
||||
def fetch(channel: str, tenant_id: int, query: dict, timeout: float = 5.0) -> UnifiedResult:
|
||||
"""Synchronous entry point. Returns UnifiedResult.
|
||||
|
||||
Args:
|
||||
channel: "amazon" | "jd" | "taobao" | "pdd" | "tiktok"
|
||||
tenant_id: chathub tenant id
|
||||
query: {"asin"|"sku"|"num_iid"|"goods_id"|"keyword": ...}
|
||||
timeout: seconds before giving up
|
||||
"""
|
||||
if not gateway_loop.loop or not gateway_loop.loop.is_running():
|
||||
return UnifiedResult(
|
||||
status="error",
|
||||
error="gateway loop not started; call gateway_loop.start() at process boot",
|
||||
channel=channel,
|
||||
)
|
||||
try:
|
||||
return gateway_loop.run(_async_fetch(channel, tenant_id, query, timeout), timeout=timeout + 2.0)
|
||||
except TimeoutError as e:
|
||||
return UnifiedResult(status="timeout", error=str(e), channel=channel)
|
||||
except RuntimeError as e:
|
||||
return UnifiedResult(status="error", error=str(e), channel=channel)
|
||||
except Exception as e:
|
||||
log.exception("fetch failed channel=%s tenant=%s", channel, tenant_id)
|
||||
return UnifiedResult(status="error", error=str(e)[:200], channel=channel)
|
||||
|
||||
|
||||
def fetch_all(
|
||||
tenant_id: int,
|
||||
query: dict,
|
||||
channels: list[str] | None = None,
|
||||
timeout: float = 5.0,
|
||||
) -> dict[str, UnifiedResult]:
|
||||
"""Fan-out to multiple channels in parallel; returns dict keyed by channel."""
|
||||
if channels is None:
|
||||
channels = list(_HANDLERS.keys())
|
||||
|
||||
if not gateway_loop.loop or not gateway_loop.loop.is_running():
|
||||
err = UnifiedResult(
|
||||
status="error",
|
||||
error="gateway loop not started; call gateway_loop.start() at process boot",
|
||||
channel="*",
|
||||
)
|
||||
return {c: err for c in channels}
|
||||
|
||||
async def _gather() -> list[UnifiedResult]:
|
||||
coros = [_async_fetch(c, tenant_id, query, timeout) for c in channels]
|
||||
return await asyncio.gather(*coros, return_exceptions=False)
|
||||
|
||||
try:
|
||||
results = gateway_loop.run(_gather(), timeout=timeout + 3.0)
|
||||
except Exception as e:
|
||||
log.exception("fetch_all failed tenant=%s", tenant_id)
|
||||
return {c: UnifiedResult(status="error", error=str(e)[:200], channel=c) for c in channels}
|
||||
return {c: r for c, r in zip(channels, results)}
|
||||
Reference in New Issue
Block a user