app.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  1. import argparse
  2. import json
  3. import sys
  4. import uuid
  5. from config import GatewayConfig
  6. from constants import DEVICE_INVALID_MESSAGE
  7. from mcp_protocol import McpProtocolHandler
  8. from public_gateway import PublicGatewayApp
  9. from public_server import serve_public
  10. from services.api_client import ApiClient
  11. from services.auth_client import AuthClient
  12. from services.gateway_session_store import GatewaySessionStore
  13. from services.diagnostic_reporter import (
  14. NullDiagnosticReporter,
  15. diagnostic_reporter_from_config,
  16. )
  17. from services.scoped_api_client import ScopedApiClient
  18. from services.token_store import FileTokenStore, RedisSocketClient, RedisTokenStore
  19. from tools.list_order_filter_options import ListOrderFilterOptionsTool
  20. from tools.list_customer_filter_options import ListCustomerFilterOptionsTool
  21. from tools.list_receivable_cost_filter_options import (
  22. ListReceivableCostFilterOptionsTool,
  23. )
  24. from tools.list_outbound_filter_options import ListOutboundFilterOptionsTool
  25. from tools.export_pending_outbound_orders import ExportPendingOutboundOrdersTool
  26. from tools.export_out_of_province_port_data import (
  27. ExportOutOfProvincePortDataTool,
  28. )
  29. from tools.export_receivable_cost_list import ExportReceivableCostListTool
  30. from tools.list_pending_outbound_export_filter_options import (
  31. ListPendingOutboundExportFilterOptionsTool,
  32. )
  33. from tools.query_order import QueryOrderTool
  34. from tools.query_customs_declaration_files import (
  35. QueryCustomsDeclarationFilesTool,
  36. )
  37. from tools.query_order_exact import QueryOrderExactTool
  38. from tools.query_order_detail import QueryOrderDetailTool
  39. from tools.query_export_task import QueryExportTaskTool
  40. from tools.query_outbound_detail import QueryOutboundDetailTool
  41. from tools.query_outbound_list import QueryOutboundListTool
  42. from tools.query_track import QueryTrackTool
  43. from tools.query_customer_list import QueryCustomerListTool
  44. from tools.query_customer_payment_followup import QueryCustomerPaymentFollowupTool
  45. from tools.query_customer_unverified_bill_details import QueryCustomerUnverifiedBillDetailsTool
  46. from tools.query_customer_payment_records import QueryCustomerPaymentRecordsTool
  47. from tools.query_order_receivable_cost_details import (
  48. QueryOrderReceivableCostDetailsTool,
  49. )
  50. from tools.query_receivable_cost_list import QueryReceivableCostListTool
  51. from tools.query_payable_cost_list import QueryPayableCostListTool
  52. from tools.list_payable_cost_filter_options import ListPayableCostFilterOptionsTool
  53. from tools.export_payable_cost_list import ExportPayableCostListTool
  54. from tools.export_pallet_data import ExportPalletDataTool
  55. from tools.query_destination_trailer_list import QueryDestinationTrailerListTool
  56. from tools.arrange_destination_trailer import ArrangeDestinationTrailerTool
  57. from tools.list_destination_trailer_filter_options import (
  58. ListDestinationTrailerFilterOptionsTool,
  59. )
  60. from tools.query_order_abnormal_list import QueryOrderAbnormalListTool
  61. from tools.list_order_abnormal_filter_options import (
  62. ListOrderAbnormalFilterOptionsTool,
  63. )
  64. from tools.query_receive_volume_list import QueryReceiveVolumeListTool
  65. from tools.list_receive_volume_filter_options import (
  66. ListReceiveVolumeFilterOptionsTool,
  67. )
  68. from tools.query_goods_info import QueryGoodsInfoTool
  69. from tools.trial_goods_tax import TrialGoodsTaxTool
  70. from tools.query_container_timeliness_list import QueryContainerTimelinessListTool
  71. from tools.export_container_timeliness_report import (
  72. ExportContainerTimelinessReportTool,
  73. )
  74. from tools.query_headhaul_document_list import QueryHeadhaulDocumentListTool
  75. from tools.list_headhaul_document_filter_options import (
  76. ListHeadhaulDocumentFilterOptionsTool,
  77. )
  78. from tools.prepare_headhaul_document_upload import (
  79. PrepareHeadhaulDocumentUploadTool,
  80. )
  81. from tools.save_headhaul_document import SaveHeadhaulDocumentTool
  82. from tools.delete_headhaul_document import DeleteHeadhaulDocumentTool
  83. from tools.list_inbound_receipt_templates import ListInboundReceiptTemplatesTool
  84. from tools.export_inbound_receipt import ExportInboundReceiptTool
  85. from tools.query_freightower_track import QueryFreightowerTrackTool
  86. from tools.query_remote_address_list import QueryRemoteAddressListTool
  87. from tools.maintain_remote_address import MaintainRemoteAddressTool
  88. from tools.save_remote_address import SaveRemoteAddressTool
  89. from tools.query_fuel_rate_list import QueryFuelRateListTool
  90. from tools.save_fuel_rate import SaveFuelRateTool
  91. from tools.enable_fuel_rate import EnableFuelRateTool
  92. def parse_int_list(value):
  93. if not value:
  94. return []
  95. return [int(item.strip()) for item in value.split(',') if item.strip()]
  96. def parse_string_list(value):
  97. result = []
  98. for item in (value or '').split(','):
  99. item = item.strip()
  100. if item and item not in result:
  101. result.append(item)
  102. return result
  103. class GatewayApp:
  104. def __init__(
  105. self,
  106. auth_client=None,
  107. api_client=None,
  108. token_store=None,
  109. reporter=None,
  110. base_api_client=None,
  111. ):
  112. self.auth_client = auth_client
  113. self.api_client = api_client
  114. self.token_store = token_store
  115. self.reporter = reporter or NullDiagnosticReporter()
  116. from services.headhaul_document_app import HeadhaulSlotMemory
  117. self._headhaul_slots = HeadhaulSlotMemory()
  118. self._tools = {
  119. 'query_order': QueryOrderTool(api_client=api_client),
  120. 'query_track': QueryTrackTool(api_client=api_client),
  121. 'query_order_exact': QueryOrderExactTool(api_client=api_client),
  122. 'query_order_detail': QueryOrderDetailTool(api_client=api_client),
  123. 'query_customs_declaration_files':
  124. QueryCustomsDeclarationFilesTool(api_client=api_client),
  125. 'query_outbound_list': QueryOutboundListTool(api_client=api_client),
  126. 'query_outbound_detail': QueryOutboundDetailTool(api_client=api_client),
  127. 'query_customer_list': QueryCustomerListTool(api_client=api_client),
  128. 'query_customer_payment_followup': QueryCustomerPaymentFollowupTool(
  129. api_client=api_client
  130. ),
  131. 'query_customer_unverified_bill_details':
  132. QueryCustomerUnverifiedBillDetailsTool(api_client=api_client),
  133. 'query_customer_payment_records': QueryCustomerPaymentRecordsTool(
  134. api_client=api_client
  135. ),
  136. 'query_order_receivable_cost_details':
  137. QueryOrderReceivableCostDetailsTool(api_client=api_client),
  138. 'query_receivable_cost_list':
  139. QueryReceivableCostListTool(api_client=api_client),
  140. 'query_payable_cost_list':
  141. QueryPayableCostListTool(api_client=api_client),
  142. 'list_outbound_filter_options': ListOutboundFilterOptionsTool(
  143. api_client=api_client
  144. ),
  145. 'list_order_filter_options': ListOrderFilterOptionsTool(
  146. api_client=api_client
  147. ),
  148. 'list_customer_filter_options': ListCustomerFilterOptionsTool(
  149. api_client=api_client
  150. ),
  151. 'list_receivable_cost_filter_options':
  152. ListReceivableCostFilterOptionsTool(api_client=api_client),
  153. 'list_payable_cost_filter_options':
  154. ListPayableCostFilterOptionsTool(api_client=api_client),
  155. 'export_pending_outbound_orders': ExportPendingOutboundOrdersTool(
  156. api_client=api_client
  157. ),
  158. 'export_out_of_province_port_data':
  159. ExportOutOfProvincePortDataTool(api_client=api_client),
  160. 'export_receivable_cost_list': ExportReceivableCostListTool(
  161. api_client=api_client
  162. ),
  163. 'export_payable_cost_list': ExportPayableCostListTool(
  164. api_client=api_client
  165. ),
  166. 'export_pallet_data': ExportPalletDataTool(
  167. api_client=api_client
  168. ),
  169. 'query_export_task': QueryExportTaskTool(api_client=api_client),
  170. 'list_pending_outbound_export_filter_options':
  171. ListPendingOutboundExportFilterOptionsTool(api_client=api_client),
  172. 'query_destination_trailer_list': QueryDestinationTrailerListTool(
  173. api_client=api_client
  174. ),
  175. 'arrange_destination_trailer': ArrangeDestinationTrailerTool(
  176. api_client=api_client
  177. ),
  178. 'list_destination_trailer_filter_options':
  179. ListDestinationTrailerFilterOptionsTool(api_client=api_client),
  180. 'query_order_abnormal_list': QueryOrderAbnormalListTool(
  181. api_client=api_client
  182. ),
  183. 'list_order_abnormal_filter_options':
  184. ListOrderAbnormalFilterOptionsTool(api_client=api_client),
  185. 'query_receive_volume_list': QueryReceiveVolumeListTool(
  186. api_client=api_client
  187. ),
  188. 'query_goods_info': QueryGoodsInfoTool(api_client=api_client),
  189. 'trial_goods_tax': TrialGoodsTaxTool(api_client=api_client),
  190. 'list_receive_volume_filter_options':
  191. ListReceiveVolumeFilterOptionsTool(api_client=api_client),
  192. 'query_container_timeliness_list': QueryContainerTimelinessListTool(
  193. api_client=api_client
  194. ),
  195. 'export_container_timeliness_report':
  196. ExportContainerTimelinessReportTool(api_client=api_client),
  197. 'query_headhaul_document_list': QueryHeadhaulDocumentListTool(
  198. api_client=api_client
  199. ),
  200. 'list_headhaul_document_filter_options':
  201. ListHeadhaulDocumentFilterOptionsTool(api_client=api_client),
  202. 'prepare_headhaul_document_upload':
  203. PrepareHeadhaulDocumentUploadTool(api_client=api_client),
  204. 'save_headhaul_document': SaveHeadhaulDocumentTool(
  205. api_client=api_client
  206. ),
  207. 'delete_headhaul_document': DeleteHeadhaulDocumentTool(
  208. api_client=api_client
  209. ),
  210. 'list_inbound_receipt_templates': ListInboundReceiptTemplatesTool(
  211. api_client=api_client
  212. ),
  213. 'export_inbound_receipt': ExportInboundReceiptTool(
  214. api_client=api_client
  215. ),
  216. 'query_freightower_track': QueryFreightowerTrackTool(
  217. api_client=api_client
  218. ),
  219. 'query_remote_address_list': QueryRemoteAddressListTool(
  220. api_client=base_api_client or api_client
  221. ),
  222. 'maintain_remote_address': MaintainRemoteAddressTool(
  223. api_client=base_api_client or api_client
  224. ),
  225. 'save_remote_address': SaveRemoteAddressTool(
  226. api_client=base_api_client or api_client
  227. ),
  228. 'query_fuel_rate_list': QueryFuelRateListTool(
  229. api_client=base_api_client or api_client
  230. ),
  231. 'save_fuel_rate': SaveFuelRateTool(
  232. api_client=base_api_client or api_client
  233. ),
  234. 'enable_fuel_rate': EnableFuelRateTool(
  235. api_client=base_api_client or api_client
  236. ),
  237. }
  238. @classmethod
  239. def from_config(cls, config, redis_client=None):
  240. if config.token_store_type == 'redis':
  241. redis = redis_client or RedisSocketClient(
  242. host=config.redis_host,
  243. port=config.redis_port,
  244. db=config.redis_db,
  245. password=config.redis_password,
  246. timeout=config.timeout_seconds,
  247. )
  248. token_store = RedisTokenStore(
  249. redis,
  250. prefix=config.redis_prefix,
  251. session_key=config.session_key,
  252. refresh_skew_seconds=config.refresh_skew_seconds,
  253. )
  254. elif config.token_store_type == 'file':
  255. token_store = FileTokenStore(
  256. config.token_store_path,
  257. refresh_skew_seconds=config.refresh_skew_seconds,
  258. )
  259. else:
  260. raise ValueError('unsupported token store type: {0}'.format(config.token_store_type))
  261. auth_client = AuthClient(
  262. base_url=config.auth_base_url,
  263. client_type=config.client_type,
  264. token_store=token_store,
  265. timeout=config.timeout_seconds,
  266. session_key=config.session_key,
  267. )
  268. api_client = ApiClient(
  269. base_url=config.tools_base_url,
  270. token_store=token_store,
  271. timeout=config.timeout_seconds,
  272. )
  273. base_api_client = ApiClient(
  274. base_url=config.auth_base_url,
  275. token_store=token_store,
  276. timeout=config.timeout_seconds,
  277. )
  278. return cls(
  279. auth_client=auth_client,
  280. api_client=api_client,
  281. token_store=token_store,
  282. reporter=diagnostic_reporter_from_config(config),
  283. base_api_client=base_api_client,
  284. )
  285. def registered_tool_names(self):
  286. return tuple(self._tools.keys())
  287. def _enabled_tool_names(self, response):
  288. if not isinstance(response, dict):
  289. raise RuntimeError('invalid enabled tool response')
  290. if response.get('code') != 'MCP_0000':
  291. raise RuntimeError(response.get('msg') or 'list enabled tools failed')
  292. data = response.get('data')
  293. codes = data.get('tool_codes') if isinstance(data, dict) else None
  294. if not isinstance(codes, list):
  295. raise RuntimeError('invalid enabled tool response')
  296. return {
  297. code.strip().lower()
  298. for code in codes
  299. if isinstance(code, str) and code.strip()
  300. }
  301. def _load_enabled_tool_names(self, request_id=''):
  302. if self.api_client is None or not hasattr(self.api_client, 'list_enabled_tools'):
  303. raise RuntimeError('enabled tool client unavailable')
  304. return self._enabled_tool_names(
  305. self.api_client.list_enabled_tools(request_id=request_id)
  306. )
  307. def list_tools(self, request_id=''):
  308. request_id = self.build_request_id(request_id)
  309. enabled = self._load_enabled_tool_names(request_id)
  310. return [
  311. tool.metadata()
  312. for name, tool in self._tools.items()
  313. if name in enabled
  314. ]
  315. def list_resources(self):
  316. return []
  317. def read_resource(self, uri, gateway_session_id='local'):
  318. return None
  319. def session_for_headhaul_token(self, token):
  320. return self._headhaul_slots.session_for_token(token)
  321. def build_request_id(self, request_id=''):
  322. request_id = str(request_id or '').strip()
  323. if request_id:
  324. return request_id
  325. return 'rq_{0}'.format(uuid.uuid4().hex[:16])
  326. def ensure_session(self):
  327. if self.token_store is None:
  328. return
  329. session = self.token_store.get()
  330. if not session or not session.get('token'):
  331. raise RuntimeError(DEVICE_INVALID_MESSAGE)
  332. if self.token_store.is_expiring():
  333. if self.auth_client is None:
  334. raise RuntimeError('mcp token expiring but auth client missing')
  335. self.auth_client.refresh(session['token'])
  336. def call_tool(self, name, arguments=None, request_id=''):
  337. if name not in self._tools:
  338. raise KeyError('tool not registered: {0}'.format(name))
  339. tool = self._tools[name]
  340. if getattr(tool, 'requires_session', True):
  341. self.ensure_session()
  342. request_id = self.build_request_id(request_id)
  343. if name not in self._load_enabled_tool_names(request_id):
  344. raise RuntimeError('tool disabled: {0}'.format(name))
  345. arguments = arguments or {}
  346. result = tool.call(request_id=request_id, **arguments)
  347. if name == 'prepare_headhaul_document_upload' and isinstance(result, dict):
  348. data = result.get('data')
  349. token = ''
  350. if isinstance(data, dict):
  351. token = str(data.get('upload_token') or '').strip()
  352. if token:
  353. self._headhaul_slots.remember('local', token)
  354. return result
  355. def create_protocol_handler(self):
  356. return McpProtocolHandler(self, reporter=self.reporter)
  357. def run_cli(self, argv=None, stdin=None, stdout=None):
  358. stdin = stdin or sys.stdin
  359. stdout = stdout or sys.stdout
  360. parser = argparse.ArgumentParser(prog='mcp-gateway')
  361. subparsers = parser.add_subparsers(dest='command', required=True)
  362. subparsers.add_parser('list-tools')
  363. subparsers.add_parser('serve-stdio')
  364. public_parser = subparsers.add_parser('serve-public')
  365. public_parser.add_argument('--host', default='0.0.0.0')
  366. public_parser.add_argument('--port', type=int, default=8765)
  367. call_parser = subparsers.add_parser('call')
  368. call_parser.add_argument('--tool', required=True)
  369. call_parser.add_argument('--keyword', default='')
  370. call_parser.add_argument('--sku', default='')
  371. call_parser.add_argument('--cn-name', default='')
  372. call_parser.add_argument('--departure-country', default='')
  373. call_parser.add_argument('--destination-country', default='')
  374. call_parser.add_argument('--price', type=float, default=0)
  375. call_parser.add_argument('--qty', type=float, default=0)
  376. call_parser.add_argument('--gross-weight', type=float, default=0)
  377. call_parser.add_argument('--box', type=float, default=0)
  378. call_parser.add_argument('--order-id', type=int, default=0)
  379. call_parser.add_argument('--order-number', default='')
  380. call_parser.add_argument('--business-type', type=int, default=0)
  381. call_parser.add_argument('--section', default='全部')
  382. call_parser.add_argument('--order-numbers', default='')
  383. call_parser.add_argument('--tracking-number', default='')
  384. call_parser.add_argument('--tracking-numbers', default='')
  385. call_parser.add_argument('--reference-number', default='')
  386. call_parser.add_argument('--reference-numbers', default='')
  387. call_parser.add_argument('--outbound-number', default='')
  388. call_parser.add_argument('--outbound-numbers', default='')
  389. call_parser.add_argument('--bl-number', default='')
  390. call_parser.add_argument('--bl-numbers', default='')
  391. call_parser.add_argument('--container-code', default='')
  392. call_parser.add_argument('--container-codes', default='')
  393. call_parser.add_argument('--template-code', default='')
  394. call_parser.add_argument('--so-number', default='')
  395. call_parser.add_argument('--so-numbers', default='')
  396. call_parser.add_argument('--shipment-id', default='')
  397. call_parser.add_argument('--receiver-country', default='')
  398. call_parser.add_argument('--product-ids', default='')
  399. call_parser.add_argument('--customer-ids', default='')
  400. call_parser.add_argument('--customer-id', type=int, default=0)
  401. call_parser.add_argument('--sub-customer-id', type=int, default=0)
  402. call_parser.add_argument('--bill-numbers', default='')
  403. call_parser.add_argument('--business-date-start', default='')
  404. call_parser.add_argument('--business-date-end', default='')
  405. call_parser.add_argument('--cost-date-start', default='')
  406. call_parser.add_argument('--cost-date-end', default='')
  407. call_parser.add_argument('--operation-date-start', default='')
  408. call_parser.add_argument('--operation-date-end', default='')
  409. call_parser.add_argument('--business-node-id', type=int, default=0)
  410. call_parser.add_argument('--provider-id', type=int, default=0)
  411. call_parser.add_argument('--cost-type-ids', default='')
  412. call_parser.add_argument('--billing-status', type=int, default=None)
  413. call_parser.add_argument('--payment-status', type=int, default=None)
  414. call_parser.add_argument('--verification-status', type=int, default=None)
  415. call_parser.add_argument('--document-type', type=int, default=None)
  416. call_parser.add_argument('--cost-type-id', type=int, default=0)
  417. call_parser.add_argument('--receive-date-start', default='')
  418. call_parser.add_argument('--receive-date-end', default='')
  419. call_parser.add_argument('--sales-id', type=int, default=0)
  420. call_parser.add_argument('--merchandiser-id', type=int, default=0)
  421. call_parser.add_argument(
  422. '--has-unverified-receivable-only',
  423. choices=('true', 'false'),
  424. default='true',
  425. )
  426. call_parser.add_argument('--warehouse-ids', default='')
  427. call_parser.add_argument('--warehouse-id', type=int, default=0)
  428. call_parser.add_argument('--department-id', type=int, default=0)
  429. call_parser.add_argument('--outbound-status', type=int, default=0)
  430. call_parser.add_argument('--shipping-method', type=int, default=0)
  431. call_parser.add_argument('--is-direct-send', type=int, default=None)
  432. call_parser.add_argument('--trailer-types', default='')
  433. call_parser.add_argument('--declaration-types', default='')
  434. call_parser.add_argument('--clearance-types', default='')
  435. call_parser.add_argument('--closing-time-start', default='')
  436. call_parser.add_argument('--closing-time-end', default='')
  437. call_parser.add_argument('--est-loading-time-start', default='')
  438. call_parser.add_argument('--est-loading-time-end', default='')
  439. call_parser.add_argument('--create-date-start', default='')
  440. call_parser.add_argument('--create-date-end', default='')
  441. call_parser.add_argument('--loading-time-start', default='')
  442. call_parser.add_argument('--loading-time-end', default='')
  443. call_parser.add_argument('--inbound-date-start', default='')
  444. call_parser.add_argument('--inbound-date-end', default='')
  445. call_parser.add_argument('--inbound-time-start', default='')
  446. call_parser.add_argument('--inbound-time-end', default='')
  447. call_parser.add_argument('--departure-time-start', default='')
  448. call_parser.add_argument('--departure-time-end', default='')
  449. call_parser.add_argument('--outbound-date-start', default='')
  450. call_parser.add_argument('--outbound-date-end', default='')
  451. call_parser.add_argument('--filter-type', default='')
  452. call_parser.add_argument('--trailer-stage', default='')
  453. call_parser.add_argument('--abnormal-status', default='')
  454. call_parser.add_argument('--sales-ids', default='')
  455. call_parser.add_argument('--merchandiser-ids', default='')
  456. call_parser.add_argument('--packing-types', default='')
  457. call_parser.add_argument('--providers-id', type=int, default=0)
  458. call_parser.add_argument('--bill-status', type=int, default=None)
  459. call_parser.add_argument('--cabinet-type', type=int, default=0)
  460. call_parser.add_argument('--eta-start', default='')
  461. call_parser.add_argument('--eta-end', default='')
  462. call_parser.add_argument('--pickup-time-start', default='')
  463. call_parser.add_argument('--pickup-time-end', default='')
  464. call_parser.add_argument('--container-return-time-start', default='')
  465. call_parser.add_argument('--container-return-time-end', default='')
  466. call_parser.add_argument('--task-ref', default='')
  467. call_parser.add_argument('--page', type=int, default=1)
  468. call_parser.add_argument('--limit', type=int, default=20)
  469. call_parser.add_argument('--request-id', default='')
  470. args = parser.parse_args(argv or [])
  471. if args.command == 'list-tools':
  472. payload = self.list_tools()
  473. elif args.command == 'serve-stdio':
  474. try:
  475. return self.create_protocol_handler().run_stdio(
  476. stdin=stdin,
  477. stdout=stdout,
  478. )
  479. finally:
  480. self.reporter.close()
  481. elif args.command == 'serve-public':
  482. config = GatewayConfig.from_env()
  483. reporter = self.reporter
  484. if isinstance(reporter, NullDiagnosticReporter):
  485. reporter = diagnostic_reporter_from_config(config)
  486. redis = RedisSocketClient(
  487. host=config.redis_host,
  488. port=config.redis_port,
  489. db=config.redis_db,
  490. password=config.redis_password,
  491. timeout=config.timeout_seconds,
  492. )
  493. session_store = GatewaySessionStore(
  494. redis,
  495. prefix=config.redis_prefix,
  496. ttl_seconds=config.gateway_session_ttl_seconds,
  497. )
  498. public_app = PublicGatewayApp(
  499. session_store=session_store,
  500. api_client=ScopedApiClient(config.tools_base_url, timeout=config.timeout_seconds),
  501. base_api_client=ScopedApiClient(
  502. config.auth_base_url,
  503. timeout=config.timeout_seconds,
  504. ),
  505. )
  506. try:
  507. return serve_public(
  508. public_app,
  509. host=args.host,
  510. port=args.port,
  511. enable_rate_limit=config.rate_limit_enabled,
  512. rate_limit_max_requests=config.rate_limit_max_requests,
  513. rate_limit_window_seconds=config.rate_limit_window_seconds,
  514. max_in_flight_per_tool=config.max_in_flight_per_tool,
  515. reporter=reporter,
  516. )
  517. finally:
  518. reporter.close()
  519. elif args.command == 'call':
  520. tool_args = {
  521. 'page': args.page,
  522. 'limit': args.limit,
  523. }
  524. if args.tool == 'query_order':
  525. if not args.keyword:
  526. raise ValueError('--keyword is required for query_order')
  527. tool_args['keyword'] = args.keyword
  528. elif args.tool == 'query_track':
  529. if args.order_id > 0:
  530. tool_args['order_id'] = args.order_id
  531. if args.order_number:
  532. tool_args['order_number'] = args.order_number
  533. if args.tracking_number:
  534. tool_args['tracking_number'] = args.tracking_number
  535. if args.order_id <= 0 and not args.order_number and not args.tracking_number:
  536. raise ValueError('--order-id, --order-number or --tracking-number is required for query_track')
  537. elif args.tool == 'query_order_exact':
  538. exact_strings = {
  539. 'order_number': args.order_number,
  540. 'reference_number': args.reference_number,
  541. 'tracking_number': args.tracking_number,
  542. 'outbound_number': args.outbound_number,
  543. 'container_code': args.container_code,
  544. 'so_number': args.so_number,
  545. 'shipment_id': args.shipment_id,
  546. 'receiver_country': args.receiver_country,
  547. 'inbound_date_start': args.inbound_date_start,
  548. 'inbound_date_end': args.inbound_date_end,
  549. 'outbound_date_start': args.outbound_date_start,
  550. 'outbound_date_end': args.outbound_date_end,
  551. }
  552. for field, value in exact_strings.items():
  553. if value:
  554. tool_args[field] = value
  555. exact_number_lists = {
  556. 'order_numbers': args.order_numbers,
  557. 'reference_numbers': args.reference_numbers,
  558. 'tracking_numbers': args.tracking_numbers,
  559. 'outbound_numbers': args.outbound_numbers,
  560. 'container_codes': args.container_codes,
  561. 'so_numbers': args.so_numbers,
  562. }
  563. for field, value in exact_number_lists.items():
  564. if value:
  565. tool_args[field] = parse_string_list(value)
  566. exact_lists = {
  567. 'product_ids': args.product_ids,
  568. 'customer_ids': args.customer_ids,
  569. 'warehouse_ids': args.warehouse_ids,
  570. }
  571. for field, value in exact_lists.items():
  572. if value:
  573. tool_args[field] = parse_int_list(value)
  574. if args.sales_id > 0:
  575. tool_args['sales_id'] = args.sales_id
  576. if args.department_id > 0:
  577. tool_args['department_id'] = args.department_id
  578. elif args.tool == 'query_order_detail':
  579. if not args.order_number:
  580. raise ValueError('--order-number is required for query_order_detail')
  581. tool_args['order_number'] = args.order_number
  582. tool_args['section'] = args.section
  583. elif args.tool == 'query_customer_list':
  584. for field, value in (
  585. ('customer_id', args.customer_id),
  586. ('department_id', args.department_id),
  587. ('sales_id', args.sales_id),
  588. ('merchandiser_id', args.merchandiser_id),
  589. ):
  590. if value > 0:
  591. tool_args[field] = value
  592. elif args.tool == 'query_customer_payment_followup':
  593. for field, value in (
  594. ('customer_id', args.customer_id),
  595. ('department_id', args.department_id),
  596. ('sales_id', args.sales_id),
  597. ('merchandiser_id', args.merchandiser_id),
  598. ):
  599. if value > 0:
  600. tool_args[field] = value
  601. tool_args['has_unverified_receivable_only'] = (
  602. args.has_unverified_receivable_only == 'true'
  603. )
  604. elif args.tool == 'query_customer_unverified_bill_details':
  605. if args.customer_id <= 0:
  606. raise ValueError(
  607. '--customer-id is required for query_customer_unverified_bill_details'
  608. )
  609. tool_args['customer_id'] = args.customer_id
  610. elif args.tool == 'query_customer_payment_records':
  611. if args.customer_id <= 0:
  612. raise ValueError(
  613. '--customer-id is required for query_customer_payment_records'
  614. )
  615. tool_args['customer_id'] = args.customer_id
  616. if args.receive_date_start:
  617. tool_args['receive_date_start'] = args.receive_date_start
  618. if args.receive_date_end:
  619. tool_args['receive_date_end'] = args.receive_date_end
  620. elif args.tool == 'query_order_receivable_cost_details':
  621. if not args.order_number:
  622. raise ValueError(
  623. '--order-number is required for query_order_receivable_cost_details'
  624. )
  625. tool_args['order_number'] = args.order_number
  626. elif args.tool in (
  627. 'query_payable_cost_list', 'export_payable_cost_list'
  628. ):
  629. if args.business_type <= 0:
  630. raise ValueError(
  631. '--business-type is required for ' + args.tool
  632. )
  633. tool_args['business_type'] = args.business_type
  634. payable_number_lists = {
  635. 'order_numbers': args.order_numbers,
  636. 'tracking_numbers': args.tracking_numbers,
  637. 'container_codes': args.container_codes,
  638. 'bl_numbers': args.bl_numbers,
  639. 'so_numbers': args.so_numbers,
  640. }
  641. for field, value in payable_number_lists.items():
  642. if value:
  643. tool_args[field] = parse_string_list(value)
  644. payable_dates = {
  645. 'business_date_start': args.business_date_start,
  646. 'business_date_end': args.business_date_end,
  647. 'cost_date_start': args.cost_date_start,
  648. 'cost_date_end': args.cost_date_end,
  649. 'operation_date_start': args.operation_date_start,
  650. 'operation_date_end': args.operation_date_end,
  651. }
  652. for field, value in payable_dates.items():
  653. if value:
  654. tool_args[field] = value
  655. for field, value in (
  656. ('business_node_id', args.business_node_id),
  657. ('provider_id', args.provider_id),
  658. ):
  659. if value > 0:
  660. tool_args[field] = value
  661. if args.cost_type_ids:
  662. tool_args['cost_type_ids'] = parse_int_list(
  663. args.cost_type_ids
  664. )
  665. for field, value in (
  666. ('billing_status', args.billing_status),
  667. ('payment_status', args.payment_status),
  668. ('verification_status', args.verification_status),
  669. ('document_type', args.document_type),
  670. ):
  671. if value is not None:
  672. tool_args[field] = value
  673. if args.tool == 'export_payable_cost_list':
  674. tool_args.pop('page', None)
  675. tool_args.pop('limit', None)
  676. elif args.tool == 'query_receivable_cost_list':
  677. for field, value in (
  678. ('reference_numbers', args.reference_numbers),
  679. ('tracking_numbers', args.tracking_numbers),
  680. ('order_numbers', args.order_numbers),
  681. ('bill_numbers', args.bill_numbers),
  682. ):
  683. if value:
  684. tool_args[field] = parse_string_list(value)
  685. for field, value in (
  686. ('business_date_start', args.business_date_start),
  687. ('business_date_end', args.business_date_end),
  688. ):
  689. if value:
  690. tool_args[field] = value
  691. for field, value in (
  692. ('customer_id', args.customer_id),
  693. ('sub_customer_id', args.sub_customer_id),
  694. ('cost_type_id', args.cost_type_id),
  695. ):
  696. if value > 0:
  697. tool_args[field] = value
  698. for field, value in (
  699. ('billing_status', args.billing_status),
  700. ('verification_status', args.verification_status),
  701. ('document_type', args.document_type),
  702. ):
  703. if value is not None:
  704. tool_args[field] = value
  705. elif args.tool == 'export_receivable_cost_list':
  706. for field, value in (
  707. ('reference_numbers', args.reference_numbers),
  708. ('tracking_numbers', args.tracking_numbers),
  709. ('order_numbers', args.order_numbers),
  710. ('bill_numbers', args.bill_numbers),
  711. ):
  712. if value:
  713. tool_args[field] = parse_string_list(value)
  714. for field, value in (
  715. ('business_date_start', args.business_date_start),
  716. ('business_date_end', args.business_date_end),
  717. ):
  718. if value:
  719. tool_args[field] = value
  720. for field, value in (
  721. ('customer_id', args.customer_id),
  722. ('sub_customer_id', args.sub_customer_id),
  723. ('cost_type_id', args.cost_type_id),
  724. ):
  725. if value > 0:
  726. tool_args[field] = value
  727. for field, value in (
  728. ('billing_status', args.billing_status),
  729. ('verification_status', args.verification_status),
  730. ('document_type', args.document_type),
  731. ):
  732. if value is not None:
  733. tool_args[field] = value
  734. tool_args.pop('page', None)
  735. tool_args.pop('limit', None)
  736. elif args.tool == 'query_customs_declaration_files':
  737. if args.outbound_numbers:
  738. tool_args['outbound_numbers'] = parse_string_list(
  739. args.outbound_numbers
  740. )
  741. if args.order_numbers:
  742. tool_args['order_numbers'] = parse_string_list(
  743. args.order_numbers
  744. )
  745. elif args.tool == 'query_outbound_list':
  746. outbound_number_lists = {
  747. 'outbound_numbers': args.outbound_numbers,
  748. 'order_numbers': args.order_numbers,
  749. 'container_codes': args.container_codes,
  750. 'so_numbers': args.so_numbers,
  751. 'bl_numbers': args.bl_numbers,
  752. }
  753. for field, value in outbound_number_lists.items():
  754. if value:
  755. tool_args[field] = parse_string_list(value)
  756. outbound_mode_lists = {
  757. 'trailer_types': args.trailer_types,
  758. 'declaration_types': args.declaration_types,
  759. 'clearance_types': args.clearance_types,
  760. }
  761. for field, value in outbound_mode_lists.items():
  762. if value:
  763. tool_args[field] = parse_int_list(value)
  764. outbound_dates = {
  765. 'closing_time_start': args.closing_time_start,
  766. 'closing_time_end': args.closing_time_end,
  767. 'est_loading_time_start': args.est_loading_time_start,
  768. 'est_loading_time_end': args.est_loading_time_end,
  769. 'create_date_start': args.create_date_start,
  770. 'create_date_end': args.create_date_end,
  771. 'loading_time_start': args.loading_time_start,
  772. 'loading_time_end': args.loading_time_end,
  773. }
  774. for field, value in outbound_dates.items():
  775. if value:
  776. tool_args[field] = value
  777. if args.outbound_status > 0:
  778. tool_args['outbound_status'] = args.outbound_status
  779. if args.shipping_method > 0:
  780. tool_args['shipping_method'] = args.shipping_method
  781. if args.warehouse_id == -1 or args.warehouse_id > 0:
  782. tool_args['warehouse_id'] = args.warehouse_id
  783. if args.is_direct_send is not None:
  784. tool_args['is_direct_send'] = args.is_direct_send
  785. elif args.tool == 'query_outbound_detail':
  786. if not args.outbound_number:
  787. raise ValueError(
  788. '--outbound-number is required for '
  789. 'query_outbound_detail'
  790. )
  791. tool_args['outbound_number'] = args.outbound_number
  792. elif args.tool == 'export_pallet_data':
  793. if args.container_codes:
  794. tool_args['container_codes'] = parse_string_list(
  795. args.container_codes
  796. )
  797. if args.bl_numbers:
  798. tool_args['bl_numbers'] = parse_string_list(
  799. args.bl_numbers
  800. )
  801. if args.inbound_time_start:
  802. tool_args['inbound_time_start'] = args.inbound_time_start
  803. if args.inbound_time_end:
  804. tool_args['inbound_time_end'] = args.inbound_time_end
  805. tool_args.pop('page', None)
  806. tool_args.pop('limit', None)
  807. elif args.tool == 'query_export_task':
  808. if not args.task_ref:
  809. raise ValueError(
  810. '--task-ref is required for query_export_task'
  811. )
  812. tool_args = {'task_ref': args.task_ref}
  813. elif args.tool == 'list_payable_cost_filter_options':
  814. if not args.filter_type:
  815. raise ValueError(
  816. '--filter-type is required for '
  817. 'list_payable_cost_filter_options'
  818. )
  819. tool_args['filter_type'] = args.filter_type
  820. tool_args['keyword'] = args.keyword
  821. if args.business_type > 0:
  822. tool_args['business_type'] = args.business_type
  823. elif args.tool == 'list_receivable_cost_filter_options':
  824. if not args.filter_type:
  825. raise ValueError(
  826. '--filter-type is required for '
  827. 'list_receivable_cost_filter_options'
  828. )
  829. tool_args['filter_type'] = args.filter_type
  830. tool_args['keyword'] = args.keyword
  831. if args.customer_id > 0:
  832. tool_args['customer_id'] = args.customer_id
  833. elif args.tool == 'query_destination_trailer_list':
  834. if not args.trailer_stage:
  835. raise ValueError(
  836. '--trailer-stage is required for '
  837. 'query_destination_trailer_list'
  838. )
  839. tool_args['trailer_stage'] = args.trailer_stage
  840. if args.bl_numbers:
  841. tool_args['bl_numbers'] = parse_string_list(args.bl_numbers)
  842. if args.container_codes:
  843. tool_args['container_codes'] = parse_string_list(
  844. args.container_codes
  845. )
  846. if args.providers_id > 0:
  847. tool_args['providers_id'] = args.providers_id
  848. if args.warehouse_id > 0:
  849. tool_args['warehouse_id'] = args.warehouse_id
  850. if args.is_direct_send is not None:
  851. tool_args['is_direct_send'] = args.is_direct_send
  852. if args.bill_status is not None:
  853. tool_args['bill_status'] = args.bill_status
  854. if args.cabinet_type > 0:
  855. tool_args['cabinet_type'] = args.cabinet_type
  856. if args.shipping_method > 0:
  857. tool_args['shipping_method'] = args.shipping_method
  858. trailer_dates = {
  859. 'eta_start': args.eta_start,
  860. 'eta_end': args.eta_end,
  861. 'pickup_time_start': args.pickup_time_start,
  862. 'pickup_time_end': args.pickup_time_end,
  863. 'container_return_time_start':
  864. args.container_return_time_start,
  865. 'container_return_time_end':
  866. args.container_return_time_end,
  867. }
  868. for field, value in trailer_dates.items():
  869. if value:
  870. tool_args[field] = value
  871. elif args.tool == 'query_order_abnormal_list':
  872. if not args.abnormal_status:
  873. raise ValueError(
  874. '--abnormal-status is required for '
  875. 'query_order_abnormal_list'
  876. )
  877. tool_args['abnormal_status'] = args.abnormal_status
  878. if args.order_numbers:
  879. tool_args['order_numbers'] = parse_string_list(
  880. args.order_numbers
  881. )
  882. elif args.tool == 'trial_goods_tax':
  883. tool_args['sku'] = args.sku
  884. tool_args['departure_country'] = args.departure_country
  885. tool_args['destination_country'] = args.destination_country
  886. tool_args['price'] = args.price
  887. tool_args['qty'] = args.qty
  888. tool_args['gross_weight'] = args.gross_weight
  889. tool_args['box'] = args.box
  890. elif args.tool == 'query_goods_info':
  891. if args.sku:
  892. tool_args['sku'] = args.sku
  893. if args.cn_name:
  894. tool_args['cn_name'] = args.cn_name
  895. if not args.sku and not args.cn_name:
  896. raise ValueError(
  897. '--sku or --cn-name is required for query_goods_info'
  898. )
  899. elif args.tool == 'query_receive_volume_list':
  900. if not args.inbound_date_start or not args.inbound_date_end:
  901. raise ValueError(
  902. '--inbound-date-start and --inbound-date-end are '
  903. 'required for query_receive_volume_list'
  904. )
  905. tool_args['inbound_date_start'] = args.inbound_date_start
  906. tool_args['inbound_date_end'] = args.inbound_date_end
  907. if args.customer_ids:
  908. tool_args['customer_ids'] = parse_int_list(args.customer_ids)
  909. if args.sales_ids:
  910. tool_args['sales_ids'] = parse_int_list(args.sales_ids)
  911. if args.merchandiser_ids:
  912. tool_args['merchandiser_ids'] = parse_int_list(
  913. args.merchandiser_ids
  914. )
  915. if args.packing_types:
  916. tool_args['packing_types'] = parse_string_list(
  917. args.packing_types
  918. )
  919. if args.product_ids:
  920. tool_args['product_ids'] = parse_int_list(args.product_ids)
  921. elif args.tool in (
  922. 'query_container_timeliness_list',
  923. 'export_container_timeliness_report',
  924. ):
  925. if args.container_codes:
  926. tool_args['container_codes'] = parse_string_list(
  927. args.container_codes
  928. )
  929. if args.bl_numbers:
  930. tool_args['bl_numbers'] = parse_string_list(args.bl_numbers)
  931. if args.departure_time_start:
  932. tool_args['departure_time_start'] = args.departure_time_start
  933. if args.departure_time_end:
  934. tool_args['departure_time_end'] = args.departure_time_end
  935. if args.tool == 'export_container_timeliness_report':
  936. tool_args.pop('page', None)
  937. tool_args.pop('limit', None)
  938. elif args.tool in (
  939. 'list_inbound_receipt_templates',
  940. 'export_inbound_receipt',
  941. ):
  942. tool_args.pop('page', None)
  943. tool_args.pop('limit', None)
  944. if not args.container_codes:
  945. raise ValueError(
  946. '--container-codes is required for ' + args.tool
  947. )
  948. tool_args['container_codes'] = parse_string_list(
  949. args.container_codes
  950. )
  951. if args.tool == 'export_inbound_receipt':
  952. if not args.template_code:
  953. raise ValueError(
  954. '--template-code is required for export_inbound_receipt'
  955. )
  956. tool_args['template_code'] = args.template_code
  957. elif args.tool == 'query_freightower_track':
  958. tool_args.pop('page', None)
  959. tool_args.pop('limit', None)
  960. if args.bl_number:
  961. tool_args['bl_number'] = args.bl_number
  962. if args.container_code:
  963. tool_args['container_code'] = args.container_code
  964. if not tool_args.get('bl_number') and not tool_args.get('container_code'):
  965. raise ValueError(
  966. '--bl-number or --container-code is required for '
  967. 'query_freightower_track'
  968. )
  969. elif args.tool in (
  970. 'list_order_filter_options', 'list_outbound_filter_options',
  971. 'list_customer_filter_options',
  972. 'list_destination_trailer_filter_options',
  973. 'list_order_abnormal_filter_options',
  974. 'list_receive_volume_filter_options',
  975. 'list_headhaul_document_filter_options',
  976. ):
  977. if not args.filter_type:
  978. raise ValueError(
  979. '--filter-type is required for '
  980. + args.tool
  981. )
  982. tool_args['filter_type'] = args.filter_type
  983. tool_args['keyword'] = args.keyword
  984. else:
  985. if args.keyword:
  986. tool_args['keyword'] = args.keyword
  987. payload = self.call_tool(
  988. args.tool,
  989. tool_args,
  990. request_id=args.request_id,
  991. )
  992. else:
  993. raise RuntimeError('unsupported command')
  994. stdout.write(json.dumps(payload, ensure_ascii=False))
  995. return 0
  996. def main(argv=None):
  997. config = GatewayConfig.from_env()
  998. app = GatewayApp.from_config(config)
  999. return app.run_cli(argv=argv)
  1000. if __name__ == '__main__':
  1001. raise SystemExit(main(sys.argv[1:]))