query_order_exact.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. class QueryOrderExactTool:
  2. name = 'query_order_exact'
  3. route_path = '/mcp/tools/queryOrderExact'
  4. STRING_FILTERS = (
  5. 'order_number',
  6. 'reference_number',
  7. 'tracking_number',
  8. 'outbound_number',
  9. 'container_code',
  10. 'so_number',
  11. 'shipment_id',
  12. 'receiver_country',
  13. 'inbound_date_start',
  14. 'inbound_date_end',
  15. 'outbound_date_start',
  16. 'outbound_date_end',
  17. )
  18. ARRAY_FILTERS = ('product_ids', 'customer_ids', 'warehouse_ids')
  19. NUMBER_ARRAY_FILTERS = (
  20. 'order_numbers',
  21. 'reference_numbers',
  22. 'tracking_numbers',
  23. 'outbound_numbers',
  24. 'container_codes',
  25. 'so_numbers',
  26. )
  27. NUMBER_FILTER_MAP = {
  28. 'order_numbers': 'order_number',
  29. 'reference_numbers': 'reference_number',
  30. 'tracking_numbers': 'tracking_number',
  31. 'outbound_numbers': 'outbound_number',
  32. 'container_codes': 'container_code',
  33. 'so_numbers': 'so_number',
  34. }
  35. SCALAR_ID_FILTERS = ('sales_id', 'department_id')
  36. MAX_NUMBER_FILTER_COUNT = 200
  37. FIELD_GUIDANCE = {
  38. 'order_number': (
  39. '系统订单号,用户明确说“订单号”且只有一个值时使用;'
  40. '不是客户参考号或快递单号。',
  41. ['USC26070917207'],
  42. ),
  43. 'order_numbers': (
  44. '多个系统订单号的批量精准查询;用户给出多个订单号时使用。',
  45. [['USC26070917207', 'USC26070917208']],
  46. ),
  47. 'reference_number': (
  48. '客户参考号,用户明确说“客户参考号”且只有一个值时使用;'
  49. '不是系统订单号。',
  50. ['REF-20260713-001'],
  51. ),
  52. 'reference_numbers': (
  53. '多个客户参考号的批量精准查询。',
  54. [['REF-001', 'REF-002']],
  55. ),
  56. 'tracking_number': (
  57. '快递单号、物流跟踪号或承运商跟踪号码;不是系统订单号。',
  58. ['1Z999AA10123456784'],
  59. ),
  60. 'tracking_numbers': (
  61. '多个快递单号或承运商跟踪号码的批量精准查询。',
  62. [['TRACK-001', 'TRACK-002']],
  63. ),
  64. 'outbound_number': (
  65. '出库单号,用户明确说“出库单号”且只有一个值时使用。',
  66. ['OUT-20260713-001'],
  67. ),
  68. 'outbound_numbers': (
  69. '多个出库单号的批量精准查询。',
  70. [['OUT-001', 'OUT-002']],
  71. ),
  72. 'container_code': (
  73. '柜号或集装箱号,只有一个值时使用。',
  74. ['MSCU1234567'],
  75. ),
  76. 'container_codes': (
  77. '多个柜号或集装箱号的批量精准查询。',
  78. [['MSCU1234567', 'TGHU7654321']],
  79. ),
  80. 'so_number': (
  81. 'SO号,即 Shipping Order 编号,只有一个值时使用。',
  82. ['SO-20260713-001'],
  83. ),
  84. 'so_numbers': (
  85. '多个 Shipping Order 编号的批量精准查询。',
  86. [['SO-001', 'SO-002']],
  87. ),
  88. 'shipment_id': (
  89. 'Shipment ID 的单值精准匹配。',
  90. ['FBA123456789'],
  91. ),
  92. 'receiver_country': (
  93. '收货国家代码;先调用 list_order_filter_options 的 country 类型'
  94. '取得可用值。',
  95. ['US'],
  96. ),
  97. 'product_ids': (
  98. '物流产品 ID 数组;必须先调用 list_order_filter_options 的 '
  99. 'product 类型取得 ID。',
  100. [[101, 102]],
  101. ),
  102. 'customer_ids': (
  103. '客户 ID 数组;必须先调用 list_order_filter_options 的 customer '
  104. '类型取得 ID。',
  105. [[201, 202]],
  106. ),
  107. 'sales_id': (
  108. '销售人员 ID;必须先调用 list_order_filter_options 的 sales 类型'
  109. '取得 ID。',
  110. [301],
  111. ),
  112. 'warehouse_ids': (
  113. '仓库 ID 数组;必须先调用 list_order_filter_options 的 warehouse '
  114. '类型取得 ID,-1 表示客户仓。',
  115. [[401, -1]],
  116. ),
  117. 'department_id': (
  118. '事业部 ID;必须先调用 list_order_filter_options 的 department '
  119. '类型取得 ID。',
  120. [501],
  121. ),
  122. 'inbound_date_start': (
  123. '入库日期范围开始日期,格式 YYYY-MM-DD。',
  124. ['2026-07-01'],
  125. ),
  126. 'inbound_date_end': (
  127. '入库日期范围结束日期,格式 YYYY-MM-DD。',
  128. ['2026-07-13'],
  129. ),
  130. 'outbound_date_start': (
  131. '出库日期范围开始日期,格式 YYYY-MM-DD。',
  132. ['2026-07-01'],
  133. ),
  134. 'outbound_date_end': (
  135. '出库日期范围结束日期,格式 YYYY-MM-DD。',
  136. ['2026-07-13'],
  137. ),
  138. 'page': ('结果页码,从 1 开始。', [1]),
  139. 'limit': ('每页记录数,范围 1 到 100。', [20]),
  140. }
  141. def __init__(self, api_client=None):
  142. self.api_client = api_client
  143. def metadata(self):
  144. properties = {
  145. field: {'type': 'string'}
  146. for field in self.STRING_FILTERS
  147. }
  148. for field in self.ARRAY_FILTERS:
  149. properties[field] = {
  150. 'type': 'array',
  151. 'items': {'type': 'integer'},
  152. }
  153. for field in self.NUMBER_ARRAY_FILTERS:
  154. properties[field] = {
  155. 'type': 'array',
  156. 'items': {'type': 'string'},
  157. }
  158. for field in self.SCALAR_ID_FILTERS:
  159. properties[field] = {'type': 'integer', 'minimum': 1}
  160. properties.update({
  161. 'page': {'type': 'integer', 'minimum': 1, 'maximum': 100},
  162. 'limit': {'type': 'integer', 'minimum': 1, 'maximum': 100},
  163. })
  164. for field, guidance in self.FIELD_GUIDANCE.items():
  165. properties[field]['description'] = guidance[0]
  166. properties[field]['examples'] = guidance[1]
  167. return {
  168. 'name': self.name,
  169. 'description': (
  170. '按用户明确指定的字段精准查询订单,并应用当前员工权限。'
  171. '订单号、客户参考号、快递单号、出库单号、柜号和 SO 号必须'
  172. '使用各自对应字段。'
  173. '单号类型不明确时先询问用户,不要调用工具猜测。'
  174. '一个值使用单数字段,多个同类值使用复数字段数组;'
  175. '同一字段使用 IN,不同字段使用 AND。'
  176. '查询无结果时不得改用其他字段重试。'
  177. '产品、客户、销售、仓库、事业部和国家条件先调用 '
  178. 'list_order_filter_options。'
  179. ),
  180. 'input_schema': {
  181. 'type': 'object',
  182. 'properties': properties,
  183. },
  184. }
  185. def call(
  186. self,
  187. order_number='',
  188. reference_number='',
  189. tracking_number='',
  190. outbound_number='',
  191. container_code='',
  192. so_number='',
  193. shipment_id='',
  194. receiver_country='',
  195. product_ids=None,
  196. customer_ids=None,
  197. sales_id=None,
  198. warehouse_ids=None,
  199. department_id=None,
  200. inbound_date_start='',
  201. inbound_date_end='',
  202. outbound_date_start='',
  203. outbound_date_end='',
  204. page=1,
  205. limit=20,
  206. request_id='rq_query_order_exact',
  207. order_numbers=None,
  208. reference_numbers=None,
  209. tracking_numbers=None,
  210. outbound_numbers=None,
  211. container_codes=None,
  212. so_numbers=None,
  213. ):
  214. if self.api_client is None:
  215. raise RuntimeError('api client is required for query_order_exact')
  216. values = locals()
  217. payload = {}
  218. for field in self.STRING_FILTERS:
  219. value = str(values[field] or '').strip()
  220. if value:
  221. payload[field] = value
  222. number_count = 0
  223. for field in self.NUMBER_ARRAY_FILTERS:
  224. value = self._normalize_string_list(values[field], field)
  225. if value:
  226. payload[field] = value
  227. singular = self.NUMBER_FILTER_MAP[field]
  228. merged = list(value)
  229. singular_value = payload.get(singular)
  230. if singular_value and singular_value not in merged:
  231. merged.insert(0, singular_value)
  232. number_count += len(merged)
  233. if number_count > self.MAX_NUMBER_FILTER_COUNT:
  234. raise ValueError('number filters must not exceed 200 items')
  235. for field in self.ARRAY_FILTERS:
  236. allow_customer_warehouse = field == 'warehouse_ids'
  237. value = self._normalize_int_list(
  238. values[field],
  239. allow_customer_warehouse=allow_customer_warehouse,
  240. )
  241. if value:
  242. payload[field] = value
  243. for field in self.SCALAR_ID_FILTERS:
  244. value = values[field]
  245. if value is None or value == '':
  246. continue
  247. value = int(value)
  248. if value <= 0:
  249. raise ValueError('{0} must be greater than 0'.format(field))
  250. payload[field] = value
  251. if not payload:
  252. raise ValueError('at least one exact order filter is required')
  253. payload['page'] = max(1, min(100, int(page)))
  254. payload['limit'] = max(1, min(100, int(limit)))
  255. return self.api_client.call_tool(
  256. self.name,
  257. self.route_path,
  258. payload,
  259. request_id,
  260. )
  261. @staticmethod
  262. def _normalize_int_list(value, allow_customer_warehouse=False):
  263. if value is None or value == '':
  264. return []
  265. if isinstance(value, str):
  266. value = [item.strip() for item in value.split(',') if item.strip()]
  267. result = []
  268. for item in value:
  269. item = int(item)
  270. if item <= 0 and not (allow_customer_warehouse and item == -1):
  271. continue
  272. if item not in result:
  273. result.append(item)
  274. return result
  275. @staticmethod
  276. def _normalize_string_list(value, field):
  277. if value is None or value == '':
  278. return []
  279. if not isinstance(value, (list, tuple)):
  280. raise ValueError('{0} must be an array'.format(field))
  281. result = []
  282. for item in value:
  283. if not isinstance(item, str):
  284. raise ValueError('{0} items must be strings'.format(field))
  285. item = item.strip()
  286. if not item or len(item) > 100:
  287. raise ValueError('{0} contains an invalid number'.format(field))
  288. if item not in result:
  289. result.append(item)
  290. return result