export_receivable_cost_list.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. from datetime import date
  2. class ExportReceivableCostListTool:
  3. name = 'export_receivable_cost_list'
  4. route_path = '/mcp/tools/exportReceivableCostList'
  5. NUMBER_FIELDS = (
  6. 'reference_numbers',
  7. 'tracking_numbers',
  8. 'order_numbers',
  9. 'bill_numbers',
  10. )
  11. def __init__(self, api_client=None):
  12. self.api_client = api_client
  13. def metadata(self):
  14. number_array = {
  15. 'type': 'array',
  16. 'minItems': 1,
  17. 'maxItems': 200,
  18. 'items': {
  19. 'type': 'string',
  20. 'minLength': 1,
  21. 'maxLength': 100,
  22. 'pattern': '.*\\S.*',
  23. },
  24. }
  25. date_field = {
  26. 'type': 'string',
  27. 'format': 'date',
  28. 'pattern': '^\\d{4}-\\d{2}-\\d{2}$',
  29. }
  30. return {
  31. 'name': self.name,
  32. 'description': (
  33. '本工具只提交异步导出任务,不会在本次调用中等待文件生成,也禁止在一次调用内'
  34. '轮询任务状态。成功后返回任务引用(task_ref)和建议等待时间'
  35. '(retry_after_seconds)。稍后单独使用 query_export_task 查询任务状态或下载链接。'
  36. '使用场景:只有用户明确要求导出应收费用单并需要下载文件时才可调用。'
  37. '筛选值必须与 query_receivable_cost_list / list_receivable_cost_filter_options'
  38. '已确认的号码类型、日期区间和筛选项 value 完全一致;禁止根据格式猜测号码类型,'
  39. '禁止勾选 ID,禁止一次调用内轮询 query_export_task。'
  40. '参数名仅用于工具调用;向用户回答时只能使用中文业务名称,不得展示内部参数名。'
  41. ),
  42. 'input_schema': {
  43. 'type': 'object',
  44. 'properties': {
  45. 'reference_numbers': dict(number_array),
  46. 'tracking_numbers': dict(number_array),
  47. 'order_numbers': dict(number_array),
  48. 'bill_numbers': dict(number_array),
  49. 'business_date_start': dict(date_field),
  50. 'business_date_end': dict(date_field),
  51. 'customer_id': {'type': 'integer', 'minimum': 1},
  52. 'sub_customer_id': {'type': 'integer', 'minimum': 1},
  53. 'billing_status': {'type': 'integer', 'enum': [0, 1]},
  54. 'verification_status': {
  55. 'type': 'integer', 'enum': [-1, 0, 1],
  56. },
  57. 'document_type': {'type': 'integer', 'minimum': 0},
  58. 'cost_type_id': {'type': 'integer', 'minimum': 1},
  59. },
  60. 'required': [],
  61. 'allOf': [
  62. {
  63. 'anyOf': [
  64. {'required': ['reference_numbers']},
  65. {'required': ['tracking_numbers']},
  66. {'required': ['order_numbers']},
  67. {'required': ['bill_numbers']},
  68. {
  69. 'required': [
  70. 'business_date_start',
  71. 'business_date_end',
  72. ],
  73. },
  74. ],
  75. },
  76. {
  77. 'if': {'required': ['sub_customer_id']},
  78. 'then': {'required': ['customer_id']},
  79. },
  80. ],
  81. 'additionalProperties': False,
  82. },
  83. }
  84. def call(
  85. self,
  86. reference_numbers=None,
  87. tracking_numbers=None,
  88. order_numbers=None,
  89. bill_numbers=None,
  90. business_date_start=None,
  91. business_date_end=None,
  92. customer_id=None,
  93. sub_customer_id=None,
  94. billing_status=None,
  95. verification_status=None,
  96. document_type=None,
  97. cost_type_id=None,
  98. request_id='rq_export_receivable_cost_list',
  99. ):
  100. if self.api_client is None:
  101. raise RuntimeError(
  102. 'api client is required for export_receivable_cost_list'
  103. )
  104. supplied_numbers = {}
  105. total_numbers = 0
  106. for field, values in (
  107. ('reference_numbers', reference_numbers),
  108. ('tracking_numbers', tracking_numbers),
  109. ('order_numbers', order_numbers),
  110. ('bill_numbers', bill_numbers),
  111. ):
  112. if values is None:
  113. continue
  114. if not isinstance(values, list) or not values:
  115. raise ValueError(field + ' must be a non-empty list')
  116. cleaned = []
  117. for value in values:
  118. if not isinstance(value, str):
  119. raise ValueError(field + ' items must be strings')
  120. item = value.strip()
  121. if not item or len(item) > 100:
  122. raise ValueError(field + ' items must be 1 to 100 chars')
  123. if item not in cleaned:
  124. cleaned.append(item)
  125. supplied_numbers[field] = cleaned
  126. total_numbers += len(cleaned)
  127. if total_numbers > 200:
  128. raise ValueError('at most 200 numbers across all number fields')
  129. has_start = business_date_start is not None
  130. has_end = business_date_end is not None
  131. if has_start != has_end:
  132. raise ValueError('business date range requires both start and end')
  133. if has_start:
  134. start = date.fromisoformat(str(business_date_start))
  135. end = date.fromisoformat(str(business_date_end))
  136. if end < start or (end - start).days > 30:
  137. raise ValueError('business date range must be within 31 days')
  138. if not supplied_numbers and not has_start:
  139. raise ValueError(
  140. 'provide number filters or a business date range'
  141. )
  142. if sub_customer_id is not None and customer_id is None:
  143. raise ValueError('sub_customer_id requires customer_id')
  144. payload = dict(supplied_numbers)
  145. if has_start:
  146. payload['business_date_start'] = str(business_date_start)
  147. payload['business_date_end'] = str(business_date_end)
  148. for field, value in (
  149. ('customer_id', customer_id),
  150. ('sub_customer_id', sub_customer_id),
  151. ('billing_status', billing_status),
  152. ('verification_status', verification_status),
  153. ('document_type', document_type),
  154. ('cost_type_id', cost_type_id),
  155. ):
  156. if value is not None:
  157. payload[field] = value
  158. return self.api_client.call_tool(
  159. self.name, self.route_path, payload, request_id
  160. )