| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- from datetime import datetime
- import re
- class ExportPalletDataTool:
- name = 'export_pallet_data'
- route_path = '/mcp/tools/exportPalletData'
- DATE_RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')
- DATETIME_RE = re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$')
- def __init__(self, api_client=None):
- self.api_client = api_client
- def metadata(self):
- number_items = {
- 'type': 'string',
- 'minLength': 1,
- 'maxLength': 100,
- 'pattern': '.*\\S.*',
- }
- number_array = {
- 'type': 'array',
- 'minItems': 1,
- 'maxItems': 200,
- 'items': dict(number_items),
- }
- time_field = {
- 'type': 'string',
- 'description': (
- '海外仓入库时间,格式 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS。'
- '只填日期时开始日 00:00:00、结束日 23:59:59。'
- ),
- }
- return {
- 'name': self.name,
- 'description': (
- '本工具只提交异步导出任务,不会在本次调用中等待文件生成,也禁止在一次调用内'
- '轮询任务状态。成功后返回任务引用(task_ref)和建议等待时间'
- '(retry_after_seconds)。稍后单独使用 query_export_task 查询任务状态或下载链接。'
- '使用场景:只有用户明确要求导出打托数据并需要下载文件时才可调用。'
- '必须提供柜号数组、提单号数组或海外仓入库时间起止,三者严格三选一,'
- '不可混用。号码类型不明确时必须先询问用户;用户没有明确号码类型时必须先询问:'
- '“请确认使用哪种号码导出:柜号还是提单号?”确认前不得调用。'
- '禁止使用:查看入仓列表、按订单号或打托批次号查询、勾选内部 ID、'
- '混用柜号/提单号/入库时间、一次调用内轮询 query_export_task。'
- '不得根据号码格式猜测,不得跨字段或跨工具试查。'
- '参数名仅用于工具调用;向用户回答时只能使用中文业务名称,'
- '不得展示内部参数名。'
- ),
- 'input_schema': {
- 'type': 'object',
- 'properties': {
- 'container_codes': dict(
- number_array,
- description=(
- '柜号数组。仅当用户明确说柜号时使用;'
- '不得放入提单号、订单号或打托批次号。'
- ),
- ),
- 'bl_numbers': dict(
- number_array,
- description=(
- '提单号数组。仅当用户明确说提单号时使用;'
- '不得放入柜号、订单号或打托批次号。'
- ),
- ),
- 'inbound_time_start': dict(time_field),
- 'inbound_time_end': dict(time_field),
- },
- 'required': [],
- 'additionalProperties': False,
- },
- }
- def call(
- self,
- container_codes=None,
- bl_numbers=None,
- inbound_time_start=None,
- inbound_time_end=None,
- request_id='rq_export_pallet_data',
- ):
- if self.api_client is None:
- raise RuntimeError('api client is required for export_pallet_data')
- payload = {}
- effective_container = self._is_effective_number_selector(container_codes)
- effective_bl = self._is_effective_number_selector(bl_numbers)
- has_start = inbound_time_start is not None
- has_end = inbound_time_end is not None
- if has_start != has_end:
- raise ValueError('inbound time range requires both start and end')
- time_mode = has_start and has_end
- number_modes = int(effective_container) + int(effective_bl)
- if number_modes > 1:
- raise ValueError('cannot mix container codes and bl numbers')
- if number_modes >= 1 and time_mode:
- raise ValueError('cannot mix numbers and inbound time')
- if effective_container:
- payload['container_codes'] = self._clean_number_list(
- 'container_codes', container_codes
- )
- elif effective_bl:
- payload['bl_numbers'] = self._clean_number_list(
- 'bl_numbers', bl_numbers
- )
- elif time_mode:
- start_bound = self._parse_bound(inbound_time_start)
- end_bound = self._parse_bound(inbound_time_end)
- if end_bound.date() < start_bound.date() or (
- end_bound.date() - start_bound.date()
- ).days > 30:
- raise ValueError('inbound time range must be within 31 days')
- payload['inbound_time_start'] = str(inbound_time_start).strip()
- payload['inbound_time_end'] = str(inbound_time_end).strip()
- elif container_codes is not None:
- self._clean_number_list('container_codes', container_codes)
- elif bl_numbers is not None:
- self._clean_number_list('bl_numbers', bl_numbers)
- else:
- raise ValueError(
- 'provide container codes, bl numbers, or an inbound time range'
- )
- return self.api_client.call_tool(
- self.name, self.route_path, payload, request_id
- )
- @staticmethod
- def _is_effective_number_selector(values):
- return isinstance(values, list) and len(values) > 0
- def _clean_number_list(self, field_name, values):
- if not isinstance(values, list) or not values:
- raise ValueError('{0} must be a non-empty list'.format(field_name))
- cleaned = []
- for value in values:
- if not isinstance(value, str):
- raise ValueError('{0} items must be strings'.format(field_name))
- item = value.strip()
- if not item or len(item) > 100:
- raise ValueError(
- '{0} items must be 1 to 100 chars'.format(field_name)
- )
- if item not in cleaned:
- cleaned.append(item)
- if len(cleaned) > 200:
- raise ValueError('at most 200 {0}'.format(field_name))
- return cleaned
- def _parse_bound(self, value):
- text = str(value).strip()
- if self.DATE_RE.match(text):
- return datetime.strptime(text, '%Y-%m-%d')
- if self.DATETIME_RE.match(text):
- return datetime.strptime(text, '%Y-%m-%d %H:%M:%S')
- raise ValueError('inbound time must be date or datetime')
|