query_customs_declaration_files.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. class QueryCustomsDeclarationFilesTool:
  2. name = 'query_customs_declaration_files'
  3. route_path = '/mcp/tools/queryCustomsDeclarationFiles'
  4. MAX_NUMBERS = 100
  5. def __init__(self, api_client=None):
  6. self.api_client = api_client
  7. def metadata(self):
  8. number_item = {
  9. 'type': 'string',
  10. 'minLength': 1,
  11. 'maxLength': 100,
  12. }
  13. return {
  14. 'name': self.name,
  15. 'description': (
  16. '批量查询订单的报关资料文件链接。用户明确说“订单号”时使用'
  17. '订单号数组;用户明确说“排舱单号”时使用排舱单号数组。'
  18. '这里的订单号是后台订单列表及排舱详情“订单号”列显示的号码,'
  19. '不是系统单号、数据库 order_id/id、客户参考号、快递单号或排舱单号。'
  20. '一次调用只能选择一种号码,单个号码也必须放入数组。'
  21. '如果用户没有明确说明是订单号还是排舱单号,必须先提问,'
  22. '让用户选择“订单号”或“排舱单号”,确认前不得调用工具。'
  23. '用户只说“单号”时必须先追问是订单号还是排舱单号。'
  24. '用户说“系统单号”时也不得当作订单号;必须先确认并取得订单号。'
  25. '不得根据号码格式猜测。'
  26. '查询无结果后不得切换号码类型,不得跨字段重试。'
  27. '参数名仅用于工具调用;向用户回答时只能使用中文业务名称,'
  28. '不得展示内部参数名。'
  29. ),
  30. 'input_schema': {
  31. 'type': 'object',
  32. 'properties': {
  33. 'outbound_numbers': {
  34. 'type': 'array',
  35. 'items': dict(number_item),
  36. 'minItems': 1,
  37. 'maxItems': self.MAX_NUMBERS,
  38. 'description': (
  39. '多个排舱单号。仅当用户明确说“排舱单号”时使用;'
  40. '不是订单号、客户参考号或快递单号。'
  41. ),
  42. 'examples': [['PC202607150001', 'PC202607150002']],
  43. },
  44. 'order_numbers': {
  45. 'type': 'array',
  46. 'items': dict(number_item),
  47. 'minItems': 1,
  48. 'maxItems': self.MAX_NUMBERS,
  49. 'description': (
  50. '多个订单号,对应后台订单列表及排舱详情“订单号”列的值。'
  51. '仅当用户明确说“订单号”时使用;不是系统单号或数据库'
  52. ' order_id/id,也不是客户参考号、快递单号或排舱单号。'
  53. '用户提供“系统单号”时不得填入此字段,必须先追问取得订单号。'
  54. ),
  55. 'examples': [['ORD202607150001', 'ORD202607150002']],
  56. },
  57. 'page': {
  58. 'type': 'integer',
  59. 'minimum': 1,
  60. 'maximum': 100,
  61. 'default': 1,
  62. 'description': '页码,从 1 开始,最大 100。',
  63. },
  64. 'limit': {
  65. 'type': 'integer',
  66. 'minimum': 1,
  67. 'maximum': 100,
  68. 'default': 20,
  69. 'description': '每页订单数量,范围 1 到 100。',
  70. },
  71. },
  72. 'oneOf': [
  73. {'required': ['outbound_numbers']},
  74. {'required': ['order_numbers']},
  75. ],
  76. 'additionalProperties': False,
  77. },
  78. }
  79. def call(
  80. self,
  81. outbound_numbers=None,
  82. order_numbers=None,
  83. page=1,
  84. limit=20,
  85. request_id='rq_query_customs_declaration_files',
  86. ):
  87. if self.api_client is None:
  88. raise RuntimeError(
  89. 'api client is required for query_customs_declaration_files'
  90. )
  91. has_outbound = outbound_numbers is not None
  92. has_order = order_numbers is not None
  93. if has_outbound == has_order:
  94. raise ValueError(
  95. 'provide outbound_numbers or order_numbers'
  96. )
  97. field = 'outbound_numbers' if has_outbound else 'order_numbers'
  98. values = outbound_numbers if has_outbound else order_numbers
  99. numbers = self._normalize_numbers(values, field)
  100. page = self._bounded_integer(page, 'page', 100)
  101. limit = self._bounded_integer(limit, 'limit', 100)
  102. return self.api_client.call_tool(
  103. self.name,
  104. self.route_path,
  105. {
  106. field: numbers,
  107. 'page': page,
  108. 'limit': limit,
  109. },
  110. request_id,
  111. )
  112. @classmethod
  113. def _normalize_numbers(cls, values, field):
  114. if not isinstance(values, list):
  115. raise ValueError('{0} must be an array'.format(field))
  116. if len(values) > cls.MAX_NUMBERS:
  117. raise ValueError('{0} must contain 1 to 100 values'.format(field))
  118. numbers = []
  119. for value in values:
  120. if not isinstance(value, str):
  121. raise ValueError('{0} must contain strings'.format(field))
  122. value = value.strip()
  123. if not value:
  124. continue
  125. if len(value) > 100:
  126. raise ValueError('{0} contains an invalid number'.format(field))
  127. if value not in numbers:
  128. numbers.append(value)
  129. if not numbers:
  130. raise ValueError('{0} must contain 1 to 100 values'.format(field))
  131. return numbers
  132. @staticmethod
  133. def _bounded_integer(value, field, maximum):
  134. if isinstance(value, bool):
  135. raise ValueError('{0} must be between 1 and {1}'.format(field, maximum))
  136. try:
  137. value = int(value)
  138. except (TypeError, ValueError):
  139. raise ValueError('{0} must be between 1 and {1}'.format(field, maximum))
  140. if value < 1 or value > maximum:
  141. raise ValueError('{0} must be between 1 and {1}'.format(field, maximum))
  142. return value