query_outbound_list.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. class QueryOutboundListTool:
  2. name = 'query_outbound_list'
  3. route_path = '/mcp/tools/queryOutboundList'
  4. MAX_NUMBERS = 100
  5. STATUS_VALUES = (20, 30, 60, 70, 80, 90, 100, 110, 120)
  6. def __init__(self, api_client=None):
  7. self.api_client = api_client
  8. def metadata(self):
  9. number_array = {
  10. 'type': 'array',
  11. 'items': {'type': 'string', 'minLength': 1, 'maxLength': 100},
  12. 'minItems': 1,
  13. 'maxItems': self.MAX_NUMBERS,
  14. }
  15. mode_array = {
  16. 'type': 'array',
  17. 'items': {'type': 'integer', 'enum': [1, 2]},
  18. 'minItems': 1,
  19. 'maxItems': 2,
  20. 'uniqueItems': True,
  21. }
  22. properties = {
  23. 'outbound_numbers': dict(
  24. number_array,
  25. description='排舱单号数组。仅当用户明确说排舱单号时使用;不得放入其他类型号码。',
  26. ),
  27. 'order_numbers': dict(
  28. number_array,
  29. description='订单号数组。仅当用户明确说订单号时使用;不得放入其他类型号码。',
  30. ),
  31. 'container_codes': dict(
  32. number_array,
  33. description='柜号或集装箱号数组。仅当用户明确说柜号时使用;不得放入其他类型号码。',
  34. ),
  35. 'so_numbers': dict(
  36. number_array,
  37. description='SO号数组。仅当用户明确说SO号时使用;不得放入其他类型号码。',
  38. ),
  39. 'bl_numbers': dict(
  40. number_array,
  41. description='提单号数组。仅当用户明确说提单号时使用;不得放入其他类型号码。',
  42. ),
  43. 'outbound_status': {
  44. 'type': 'integer', 'enum': list(self.STATUS_VALUES),
  45. 'description': (
  46. '排舱阶段筛选值。先调用list_outbound_filter_options并选择“排舱阶段”'
  47. '取得后台中文阶段和可传值;可选阶段为国内排舱、国内拖车、出库装柜、'
  48. '出口报关、干线运输、目的国清关、海外拖车、海外仓入库、完成。'
  49. '用户未明确时必须先询问,不得默认选择。'
  50. ),
  51. },
  52. 'shipping_method': {
  53. 'type': 'integer', 'enum': [1, 2, 3],
  54. 'description': (
  55. '运输方式筛选值。明确指定时先通过统一筛选选项工具取得;未指定时查询全部,'
  56. '调用工具时不传该筛选。'
  57. ),
  58. },
  59. 'warehouse_id': {
  60. 'oneOf': [
  61. {'type': 'integer', 'const': -1},
  62. {'type': 'integer', 'minimum': 1},
  63. ],
  64. 'description': (
  65. '集货仓库筛选值。必须先调用list_outbound_filter_options并选择'
  66. '“集货仓库”取得;禁止猜测内部值。'
  67. ),
  68. },
  69. 'is_direct_send': dict(
  70. {'type': 'integer', 'enum': [0, 1]},
  71. description='是否直送柜筛选值,先通过统一筛选选项工具取得。',
  72. ),
  73. 'trailer_types': dict(mode_array, description='拖车方式筛选值,先通过统一筛选选项工具取得。'),
  74. 'declaration_types': dict(mode_array, description='报关方式筛选值,先通过统一筛选选项工具取得。'),
  75. 'clearance_types': dict(mode_array, description='清关方式筛选值,先通过统一筛选选项工具取得。'),
  76. 'page': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 1},
  77. 'limit': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 20},
  78. }
  79. for field, label in (
  80. ('closing_time_start', '截关开始时间'),
  81. ('closing_time_end', '截关结束时间'),
  82. ('est_loading_time_start', '预计装柜开始时间'),
  83. ('est_loading_time_end', '预计装柜结束时间'),
  84. ('create_date_start', '创建开始时间'),
  85. ('create_date_end', '创建结束时间'),
  86. ('loading_time_start', '出库开始时间'),
  87. ('loading_time_end', '出库结束时间'),
  88. ):
  89. properties[field] = {
  90. 'type': 'string', 'maxLength': 19,
  91. 'description': label + ',格式与后台筛选一致。',
  92. }
  93. return {
  94. 'name': self.name,
  95. 'description': (
  96. '使用场景:用户明确要查看或筛选排舱列表时使用,返回后台排舱单列表权限'
  97. '范围内固定的19个字段。运输方式未指定时查询全部;排舱阶段必须由用户明确,未明确时'
  98. '必须先询问,不得默认选择。号码类型不明确时必须先'
  99. '询问用户,确认是排舱单号、订单号、柜号、SO号还是提单号后再调用。'
  100. '只有用户明确分别提供多个不同类型条件时才按AND组合。阶段、运输方式、仓库、'
  101. '直送柜、拖车、报关和清关筛选均先调用list_outbound_filter_options取得可传值。'
  102. '禁止使用:用户要查看订单资料、排舱详情、物流轨迹、报关资料文件或导出'
  103. '结果时不得使用本工具。不得根据号码格式猜测,不得跨字段或跨工具试查,'
  104. '查询无结果后不得换号码字段重试。'
  105. '公司、员工和权限范围由当前设备会话确定,调用方不得覆盖。'
  106. '参数只用于工具内部调用;描述筛选条件时只能使用中文业务名称,不得展示筛选'
  107. '字段的英文参数名,不得展示内部参数名,不得使用“筛选字段=内部值”形式,'
  108. '不得展示筛选项内部数字'
  109. '代码,不得使用“API固定值”“接口参数”“默认参数”等技术表述。查询结果中的'
  110. '件数、重量、体积、日期等正常业务数值必须保留。'
  111. ),
  112. 'input_schema': {
  113. 'type': 'object',
  114. 'properties': properties,
  115. 'required': ['outbound_status'],
  116. 'additionalProperties': False,
  117. },
  118. }
  119. def call(
  120. self,
  121. outbound_numbers=None,
  122. order_numbers=None,
  123. container_codes=None,
  124. so_numbers=None,
  125. bl_numbers=None,
  126. outbound_status=None,
  127. shipping_method=None,
  128. warehouse_id=None,
  129. is_direct_send=None,
  130. trailer_types=None,
  131. declaration_types=None,
  132. clearance_types=None,
  133. closing_time_start='',
  134. closing_time_end='',
  135. est_loading_time_start='',
  136. est_loading_time_end='',
  137. create_date_start='',
  138. create_date_end='',
  139. loading_time_start='',
  140. loading_time_end='',
  141. page=1,
  142. limit=20,
  143. request_id='rq_query_outbound_list',
  144. ):
  145. if self.api_client is None:
  146. raise RuntimeError('api client is required for query_outbound_list')
  147. payload = {
  148. 'outbound_status': self._enum_integer(
  149. outbound_status, 'outbound_status', self.STATUS_VALUES
  150. ),
  151. 'page': self._bounded_integer(page, 'page', 100),
  152. 'limit': self._bounded_integer(limit, 'limit', 100),
  153. }
  154. if shipping_method is not None:
  155. payload['shipping_method'] = self._enum_integer(
  156. shipping_method, 'shipping_method', (1, 2, 3)
  157. )
  158. for field, values in (
  159. ('outbound_numbers', outbound_numbers),
  160. ('order_numbers', order_numbers),
  161. ('container_codes', container_codes),
  162. ('so_numbers', so_numbers),
  163. ('bl_numbers', bl_numbers),
  164. ):
  165. if values is not None:
  166. payload[field] = self._string_array(values, field)
  167. for field, values in (
  168. ('trailer_types', trailer_types),
  169. ('declaration_types', declaration_types),
  170. ('clearance_types', clearance_types),
  171. ):
  172. if values is not None:
  173. payload[field] = self._mode_array(values, field)
  174. if warehouse_id is not None:
  175. payload['warehouse_id'] = self._warehouse_id(warehouse_id)
  176. if is_direct_send is not None:
  177. payload['is_direct_send'] = self._enum_integer(
  178. is_direct_send, 'is_direct_send', (0, 1)
  179. )
  180. for field, value in (
  181. ('closing_time_start', closing_time_start),
  182. ('closing_time_end', closing_time_end),
  183. ('est_loading_time_start', est_loading_time_start),
  184. ('est_loading_time_end', est_loading_time_end),
  185. ('create_date_start', create_date_start),
  186. ('create_date_end', create_date_end),
  187. ('loading_time_start', loading_time_start),
  188. ('loading_time_end', loading_time_end),
  189. ):
  190. value = str(value or '').strip()
  191. if len(value) > 19:
  192. raise ValueError('{0} is too long'.format(field))
  193. if value:
  194. payload[field] = value
  195. return self.api_client.call_tool(
  196. self.name, self.route_path, payload, request_id
  197. )
  198. @classmethod
  199. def _string_array(cls, values, field):
  200. if not isinstance(values, list) or len(values) > cls.MAX_NUMBERS:
  201. raise ValueError('{0} must contain 1 to 100 values'.format(field))
  202. result = []
  203. for value in values:
  204. if not isinstance(value, str):
  205. raise ValueError('{0} must contain strings'.format(field))
  206. value = value.strip()
  207. if not value or len(value) > 100:
  208. raise ValueError('{0} contains an invalid value'.format(field))
  209. if value not in result:
  210. result.append(value)
  211. if not result:
  212. raise ValueError('{0} must contain 1 to 100 values'.format(field))
  213. return result
  214. @classmethod
  215. def _mode_array(cls, values, field):
  216. if not isinstance(values, list) or not values or len(values) > 2:
  217. raise ValueError('{0} must contain 1 or 2'.format(field))
  218. result = []
  219. for value in values:
  220. value = cls._enum_integer(value, field, (1, 2))
  221. if value not in result:
  222. result.append(value)
  223. return result
  224. @staticmethod
  225. def _bounded_integer(value, field, maximum):
  226. if isinstance(value, bool):
  227. raise ValueError('{0} is invalid'.format(field))
  228. try:
  229. value = int(value)
  230. except (TypeError, ValueError):
  231. raise ValueError('{0} is invalid'.format(field))
  232. if value < 1 or value > maximum:
  233. raise ValueError('{0} is invalid'.format(field))
  234. return value
  235. @staticmethod
  236. def _enum_integer(value, field, allowed):
  237. if isinstance(value, bool):
  238. raise ValueError('{0} is invalid'.format(field))
  239. try:
  240. value = int(value)
  241. except (TypeError, ValueError):
  242. raise ValueError('{0} is invalid'.format(field))
  243. if value not in allowed:
  244. raise ValueError('{0} is invalid'.format(field))
  245. return value
  246. @staticmethod
  247. def _warehouse_id(value):
  248. if isinstance(value, bool):
  249. raise ValueError('warehouse_id is invalid')
  250. try:
  251. value = int(value)
  252. except (TypeError, ValueError):
  253. raise ValueError('warehouse_id is invalid')
  254. if value != -1 and value < 1:
  255. raise ValueError('warehouse_id is invalid')
  256. return value