query_customs_declaration_files.py 6.9 KB

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