| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- import json
- import logging
- from constants import DEVICE_INVALID_MESSAGE
- logger = logging.getLogger(__name__)
- # Builds AI-safe MCP results for the explicitly supported tool set.
- class OutputPresenter:
- TABLE_TOOLS = frozenset((
- 'query_order_exact',
- 'query_track',
- 'query_customs_declaration_files',
- ))
- OPTION_TOOLS = frozenset((
- 'list_order_filter_options',
- 'list_pending_outbound_export_filter_options',
- ))
- EXPORT_TOOLS = frozenset((
- 'export_pending_outbound_orders',
- 'export_out_of_province_port_data',
- ))
- SAFE_TOOLS = TABLE_TOOLS | OPTION_TOOLS | EXPORT_TOOLS
- TABLE_COLUMNS = {
- 'query_order_exact': {
- 'order_number': ('订单号',),
- 'reference_number': ('客户参考号',),
- 'status_txt_name': ('状态',),
- 'check_status_txt_name': ('是否已查验',),
- 'customer_name': ('客户名称',),
- 'customer_account_type_name': ('客户属性',),
- 'inbound_date': ('入库时间',),
- 'wo_num': ('未完成工单',),
- 'product_name': ('物流产品',),
- 'inbound_pieces': ('件数',),
- 'inbound_volume': ('体积(CBM)',),
- 'inbound_weight': ('重量(KG)',),
- 'pro_cn_name': ('品名',),
- 'export_declaration_type': ('报关方式',),
- 'merge_declare_number': ('合并报关单号',),
- 'delivery_address': ('派送地址',),
- 'container_code': ('柜号',),
- 'out_status_txt': ('排舱单状态',),
- 'hinge_of_destination': ('目的港',),
- 'etd': ('ETD',),
- 'atd': ('ATD',),
- 'eta': ('ETA',),
- 'ata': ('ATA',),
- 'release_time': ('清关放行时间',),
- 'oversea_inbound_date': ('海外入库时间',),
- 'appt_time': ('APPT时间',),
- 'est_loading_time': ('预计装柜时间',),
- 'pickup_time': ('海外提柜时间',),
- 'delivery_way_title': ('派送方式',),
- 'tracking_number': ('快递单号', '承运商跟踪号码'),
- 'shipment_id': ('Shipment ID',),
- 'goods_attribute': ('商品属性',),
- 'sku': ('SKU',),
- 'sales_user': ('商务经理',),
- 'service_user': ('客户经理',),
- 'department_name': ('事业部',),
- 'remark': ('订单备注',),
- 'importer_name': ('进口商',),
- 'warehouse_name': ('交货仓库',),
- 'paid_status_name': ('付款状态',),
- },
- 'query_track': {
- 'status': ('轨迹节点', '轨迹状态,如:开始集港、离港放行、清关、配送等'),
- 'location': ('轨迹地点', '发生地点'),
- 'time': ('时间', '轨迹发生时间(已转换为用户时区)'),
- 'content': ('轨迹内容', '详细描述'),
- 'tracking_number': ('跟踪号', '快递单号'),
- 'shipment_id': ('Shipment ID', '包裹ID'),
- },
- 'query_customs_declaration_files': {
- 'outbound_number': ('排舱单号',),
- 'order_number': ('订单号',),
- 'file_name': ('文件名',),
- 'file_type': ('文件类型',),
- 'file_url': ('文件链接',),
- },
- }
- FIELD_LABELS = {
- 'query_order_exact': {
- 'order_number': '订单号',
- 'order_numbers': '订单号',
- 'reference_number': '客户参考号',
- 'reference_numbers': '客户参考号',
- 'tracking_number': '快递单号',
- 'tracking_numbers': '快递单号',
- 'outbound_number': '排舱单号',
- 'outbound_numbers': '排舱单号',
- 'container_code': '柜号',
- 'container_codes': '柜号',
- 'so_number': 'SO号',
- 'so_numbers': 'SO号',
- 'shipment_id': 'Shipment ID',
- 'receiver_country': '收货国家',
- 'product_ids': '物流产品',
- 'customer_ids': '客户',
- 'sales_id': '销售人员',
- 'warehouse_ids': '仓库',
- 'department_id': '事业部',
- 'inbound_date_start': '入库开始日期',
- 'inbound_date_end': '入库结束日期',
- 'outbound_date_start': '出库开始日期',
- 'outbound_date_end': '出库结束日期',
- 'page': '页码',
- 'limit': '每页数量',
- },
- 'query_track': {
- 'order_id': '订单ID',
- 'order_number': '订单号',
- 'tracking_number': '快递单号',
- 'page': '页码',
- 'limit': '每页数量',
- },
- 'query_customs_declaration_files': {
- 'outbound_numbers': '排舱单号',
- 'order_numbers': '订单号',
- 'page': '页码',
- 'limit': '每页数量',
- },
- 'list_order_filter_options': {
- 'filter_type': '筛选项类型',
- 'keyword': '关键词',
- 'page': '页码',
- 'limit': '每页数量',
- },
- 'list_pending_outbound_export_filter_options': {
- 'filter_type': '筛选项类型',
- 'product_type_id': '产品分类',
- 'keyword': '关键词',
- 'page': '页码',
- 'limit': '每页数量',
- },
- 'export_pending_outbound_orders': {
- 'number': '单号',
- 'product_type_id': '产品分类',
- 'product_id': '物流产品',
- 'order_warehouse_id': '集货仓库',
- 'receiver_country': '目的国',
- 'is_remove': '是否装柜剔除',
- 'address': '派送地址',
- 'inbound_date': '入库时间',
- 'status': '订单状态',
- 'merge_declare_number': '合并报关单号',
- 'importer_id': '进口商',
- 'packing_type': '货物类型',
- 'is_battery': '带电属性',
- 'is_magnetic': '带磁属性',
- 'is_wood': '带木属性',
- 'is_other': '其他商品属性',
- 'is_fda': 'FDA产品属性',
- 'is_toy': '玩具属性',
- 'is_ultra_limit': '超长超重属性',
- 'is_sensitive': '敏感货属性',
- 'is_food': '食品属性',
- 'no_property': '无属性',
- },
- 'export_out_of_province_port_data': {
- 'outbound_numbers': '排舱单号',
- 'container_codes': '柜号',
- 'bl_numbers': '提单号',
- 'so_numbers': 'SO号',
- 'file_type': '资料类型',
- },
- }
- ERROR_MESSAGES = {
- 'MCP_1101': '设备配置已失效,请重新生成配置',
- 'MCP_1102': '设备会话已过期,请重新连接',
- 'MCP_1103': '员工账号不可用,请联系管理员',
- 'MCP_1104': '身份信息已失效,请重新登录或重新生成设备配置',
- 'MCP_1201': '工具当前不可用',
- 'MCP_1202': '工具当前不可用',
- 'MCP_1301': '没有权限使用此工具',
- 'MCP_1501': '目标数据不可用',
- 'MCP_1502': '结果过多,请缩小查询范围',
- 'MCP_1601': '没有可导出的数据',
- 'MCP_9001': '工具调用失败,请稍后重试',
- }
- NON_RETRYABLE_CODES = frozenset((
- 'MCP_1101',
- 'MCP_1102',
- 'MCP_1103',
- 'MCP_1104',
- 'MCP_1201',
- 'MCP_1202',
- 'MCP_1301',
- 'MCP_1401',
- 'MCP_1501',
- 'MCP_1502',
- 'MCP_1601',
- ))
- def handles(self, tool_name):
- return isinstance(tool_name, str) and tool_name in self.SAFE_TOOLS
- # Convert a backend envelope into text and structured display data.
- def present(self, tool_name, tool_result):
- if not self.handles(tool_name) or not isinstance(tool_result, dict):
- return self._format_error()
- code = str(tool_result.get('code') or '').strip()
- meta = self._build_meta(tool_result.get('meta'))
- if code not in ('MCP_0000', '0'):
- return self._business_error(
- tool_name,
- code or 'MCP_9001',
- tool_result.get('msg'),
- meta,
- )
- data = tool_result.get('data')
- if not isinstance(data, dict):
- return self._format_error(meta)
- if tool_name in self.TABLE_TOOLS:
- return self._present_table(
- tool_name,
- data,
- tool_result.get('meta'),
- meta,
- )
- if tool_name in self.OPTION_TOOLS:
- return self._present_options(data, tool_result.get('meta'), meta)
- return self._present_export(data, meta)
- # Map local tool exceptions without exposing internal error details.
- def present_exception(self, tool_name, exception):
- if not self.handles(tool_name):
- return self._format_error()
- message = str(exception or '').strip()
- if isinstance(exception, ValueError):
- label = self._find_field_label(tool_name, message)
- return self._error_result(
- 'MCP_1401',
- self._parameter_message(label),
- False,
- )
- if message == DEVICE_INVALID_MESSAGE:
- return self._error_result('MCP_1101', DEVICE_INVALID_MESSAGE, False)
- if message.lower().startswith('tool disabled'):
- return self._error_result('MCP_1202', '工具当前不可用', False)
- return self._error_result(
- 'MCP_9001',
- '工具调用失败,请稍后重试',
- True,
- )
- def _present_table(self, tool_name, data, raw_meta, meta):
- columns = data.get('columns')
- records = data.get('records')
- if not isinstance(columns, list) or not columns or not isinstance(records, list):
- return self._format_error(meta)
- allowed_columns = self.TABLE_COLUMNS.get(tool_name)
- if not isinstance(allowed_columns, dict):
- return self._format_error(meta)
- headers = []
- keys = []
- for column in columns:
- if not isinstance(column, dict):
- return self._format_error(meta)
- key = column.get('key')
- label = column.get('name')
- if not isinstance(key, str) or not key.strip():
- return self._format_error(meta)
- if not isinstance(label, str) or not label.strip():
- return self._format_error(meta)
- key = key.strip()
- definition = allowed_columns.get(key)
- if not isinstance(definition, tuple) or not definition:
- return self._format_error(meta)
- keys.append(key)
- header = {'label': definition[0]}
- if len(definition) > 1:
- header['description'] = definition[1]
- headers.append(header)
- rows = []
- for record in records:
- if not isinstance(record, dict):
- return self._format_error(meta)
- rows.append([
- '' if record.get(key) is None else record.get(key, '')
- for key in keys
- ])
- content = {
- 'summary': self._safe_text(data.get('summary')),
- 'headers': headers,
- 'rows': rows,
- }
- tips = self._safe_tips(data.get('tips'))
- if tips:
- content['tips'] = tips
- pagination = self._build_pagination(raw_meta)
- if pagination:
- content['pagination'] = pagination
- return self._success_result(content, self._render_table(content), meta)
- def _present_options(self, data, raw_meta, meta):
- records = data.get('records')
- if not isinstance(records, list):
- return self._format_error(meta)
- rows = []
- for record in records:
- if not isinstance(record, dict):
- return self._format_error(meta)
- rows.append([
- record.get('value', ''),
- self._safe_text(record.get('label')),
- self._safe_text(record.get('code')),
- ])
- content = {
- 'headers': [
- {'label': '可传值'},
- {'label': '显示名称'},
- {'label': '业务编码'},
- ],
- 'rows': rows,
- }
- pagination = self._build_pagination(raw_meta)
- if pagination:
- content['pagination'] = pagination
- return self._success_result(content, self._render_table(content), meta)
- def _present_export(self, data, meta):
- url = data.get('file_url')
- if not isinstance(url, str) or not url.strip():
- return self._format_error(meta)
- content = {
- 'message': '文件已生成',
- 'files': [{'label': '导出文件', 'url': url.strip()}],
- }
- text = '文件已生成\n- 导出文件: {0}'.format(url.strip())
- return self._success_result(content, text, meta)
- def _business_error(self, tool_name, code, raw_message, meta):
- if code not in self.ERROR_MESSAGES and code != 'MCP_1401':
- logger.warning(
- 'Unknown MCP business error code',
- extra={
- 'request_id': meta.get('request_id', ''),
- 'tool_code': tool_name,
- 'backend_code': code,
- 'response_code': 'MCP_9001',
- 'diagnostic_reason': 'UNEXPECTED_EXCEPTION',
- },
- )
- code = 'MCP_9001'
- if code == 'MCP_1401':
- label = self._find_field_label(tool_name, raw_message)
- message = self._parameter_message(label)
- else:
- message = self.ERROR_MESSAGES[code]
- retryable = code not in self.NON_RETRYABLE_CODES
- return self._error_result(code, message, retryable, meta)
- def _find_field_label(self, tool_name, raw_message):
- message = str(raw_message or '')
- fields = self.FIELD_LABELS.get(tool_name, {})
- for field in sorted(fields, key=len, reverse=True):
- if field in message:
- return fields[field]
- return ''
- @staticmethod
- def _parameter_message(label):
- return '{0}参数不正确'.format(label) if label else '工具参数不正确,请检查后重试'
- @staticmethod
- def _build_meta(raw_meta):
- if not isinstance(raw_meta, dict):
- return {}
- request_id = raw_meta.get('request_id')
- if not isinstance(request_id, str) or not request_id.strip():
- return {}
- return {'request_id': request_id.strip()}
- @staticmethod
- def _build_pagination(raw_meta):
- if not isinstance(raw_meta, dict):
- return {}
- return {
- key: raw_meta[key]
- for key in ('page', 'limit', 'has_more', 'total')
- if key in raw_meta
- }
- @staticmethod
- def _safe_text(value):
- if value is None:
- return ''
- return str(value).strip()
- @classmethod
- def _safe_tips(cls, value):
- if not isinstance(value, list):
- return []
- return [cls._safe_text(item) for item in value if cls._safe_text(item)]
- @classmethod
- def _render_table(cls, content):
- headers = content.get('headers') or []
- rows = content.get('rows') or []
- lines = []
- summary = cls._safe_text(content.get('summary'))
- if summary:
- lines.append(summary)
- lines.append('表头共 {0} 列:'.format(len(headers)))
- for index, header in enumerate(headers, start=1):
- lines.append('{0}. {1}'.format(index, header['label']))
- for index, row in enumerate(rows, start=1):
- lines.append('记录 {0}:'.format(index))
- for header, value in zip(headers, row):
- if isinstance(value, (dict, list)):
- value = json.dumps(value, ensure_ascii=False)
- lines.append('- {0}: {1}'.format(header['label'], value))
- tips = content.get('tips') or []
- if tips:
- lines.append('提示:{0}'.format(';'.join(tips)))
- return '\n'.join(lines)
- @staticmethod
- def _success_result(content, text, meta):
- return {
- 'structured_content': content,
- 'text': text,
- 'is_error': False,
- 'meta': meta,
- }
- @staticmethod
- def _error_result(code, message, retryable, meta=None):
- return {
- 'structured_content': {
- 'code': code,
- 'message': message,
- 'retryable': retryable,
- },
- 'text': '{0}: {1}'.format(code, message),
- 'is_error': True,
- 'meta': meta or {},
- }
- @classmethod
- def _format_error(cls, meta=None):
- return cls._error_result(
- 'MCP_9001',
- '工具返回格式异常',
- True,
- meta,
- )
|