瀏覽代碼

导出排舱单mcp接口

jackson 1 周之前
父節點
當前提交
264c922cc9

+ 9 - 0
app.py

@@ -14,6 +14,10 @@ from services.gateway_session_store import GatewaySessionStore
 from services.scoped_api_client import ScopedApiClient
 from services.token_store import FileTokenStore, RedisSocketClient, RedisTokenStore
 from tools.list_order_filter_options import ListOrderFilterOptionsTool
+from tools.export_pending_outbound_orders import ExportPendingOutboundOrdersTool
+from tools.list_pending_outbound_export_filter_options import (
+    ListPendingOutboundExportFilterOptionsTool,
+)
 from tools.query_order import QueryOrderTool
 from tools.query_order_exact import QueryOrderExactTool
 from tools.query_track import QueryTrackTool
@@ -46,6 +50,11 @@ class GatewayApp:
             'list_order_filter_options': ListOrderFilterOptionsTool(
                 api_client=api_client
             ),
+            'export_pending_outbound_orders': ExportPendingOutboundOrdersTool(
+                api_client=api_client
+            ),
+            'list_pending_outbound_export_filter_options':
+                ListPendingOutboundExportFilterOptionsTool(api_client=api_client),
         }
 
     @classmethod

+ 9 - 0
public_gateway.py

@@ -3,6 +3,10 @@ import uuid
 
 from constants import DEVICE_INVALID_MESSAGE
 from tools.list_order_filter_options import ListOrderFilterOptionsTool
+from tools.export_pending_outbound_orders import ExportPendingOutboundOrdersTool
+from tools.list_pending_outbound_export_filter_options import (
+    ListPendingOutboundExportFilterOptionsTool,
+)
 from tools.query_order import QueryOrderTool
 from tools.query_order_exact import QueryOrderExactTool
 from tools.query_track import QueryTrackTool
@@ -23,6 +27,11 @@ class PublicGatewayApp:
             'list_order_filter_options': ListOrderFilterOptionsTool(
                 api_client=None
             ),
+            'export_pending_outbound_orders': ExportPendingOutboundOrdersTool(
+                api_client=None
+            ),
+            'list_pending_outbound_export_filter_options':
+                ListPendingOutboundExportFilterOptionsTool(api_client=None),
         }
 
     def registered_tool_names(self):

+ 153 - 0
tests/test_export_pending_outbound_tools.py

@@ -0,0 +1,153 @@
+import unittest
+
+from app import GatewayApp
+from public_gateway import PublicGatewayApp
+from tools.export_pending_outbound_orders import ExportPendingOutboundOrdersTool
+from tools.list_pending_outbound_export_filter_options import (
+    ListPendingOutboundExportFilterOptionsTool,
+)
+
+
+class RecordingApiClient:
+    def __init__(self):
+        self.calls = []
+
+    def call_tool(self, tool_code, route_path, payload, request_id):
+        self.calls.append((tool_code, route_path, payload, request_id))
+        return {'code': 'MCP_0000', 'data': {'file_url': 'https://files.test/order.xlsx'}}
+
+    def list_enabled_tools(self):
+        return {
+            'code': 'MCP_0000',
+            'data': {
+                'tool_codes': [
+                    'export_pending_outbound_orders',
+                    'list_pending_outbound_export_filter_options',
+                ],
+            },
+        }
+
+
+class PublicSessionStore:
+    def get(self, gateway_session_id):
+        return {'mcp_token': 'MT_test'} if gateway_session_id == 'GWS_test' else None
+
+
+class PublicApiClient:
+    def list_enabled_tools(self, token):
+        return {
+            'code': 'MCP_0000',
+            'data': {
+                'tool_codes': [
+                    'export_pending_outbound_orders',
+                    'list_pending_outbound_export_filter_options',
+                ],
+            },
+        }
+
+
+class ExportPendingOutboundToolsTest(unittest.TestCase):
+    def test_metadata_uses_add_page_filter_names_and_native_types(self):
+        schema = ExportPendingOutboundOrdersTool().metadata()['input_schema']
+        properties = schema['properties']
+
+        for field in (
+            'number', 'product_type_id', 'product_id', 'order_warehouse_id',
+            'receiver_country', 'is_remove', 'address', 'inbound_date', 'status',
+            'is_battery', 'is_magnetic', 'is_wood', 'is_other', 'is_fda',
+            'is_toy', 'is_ultra_limit', 'is_sensitive', 'is_food', 'no_property',
+            'merge_declare_number', 'importer_id', 'packing_type',
+        ):
+            self.assertIn(field, properties)
+
+        self.assertEqual('array', properties['product_id']['type'])
+        self.assertEqual('array', properties['status']['type'])
+        self.assertEqual('string', properties['packing_type']['type'])
+        self.assertNotIn('property_2', properties)
+
+    def test_export_call_preserves_three_page_multi_select_shapes(self):
+        client = RecordingApiClient()
+        tool = ExportPendingOutboundOrdersTool(api_client=client)
+
+        result = tool.call(
+            product_id=[12, 13],
+            status=[30, 40],
+            is_battery='Y',
+            packing_type='散货,整柜',
+            is_remove=0,
+            request_id='rq_export',
+        )
+
+        self.assertEqual('MCP_0000', result['code'])
+        self.assertEqual(
+            {
+                'product_id': [12, 13],
+                'status': [30, 40],
+                'is_battery': 'Y',
+                'packing_type': '散货,整柜',
+                'is_remove': 0,
+            },
+            client.calls[0][2],
+        )
+        self.assertEqual('/mcp/tools/exportPendingOutboundOrders', client.calls[0][1])
+
+    def test_filter_options_forwards_product_type_dependency_and_bounds_page(self):
+        client = RecordingApiClient()
+        tool = ListPendingOutboundExportFilterOptionsTool(api_client=client)
+
+        tool.call(
+            filter_type=' product ',
+            product_type_id=8,
+            keyword=' 海运 ',
+            page=0,
+            limit=200,
+            request_id='rq_filters',
+        )
+
+        self.assertEqual(
+            {
+                'filter_type': 'product',
+                'product_type_id': 8,
+                'keyword': '海运',
+                'page': 1,
+                'limit': 100,
+            },
+            client.calls[0][2],
+        )
+        self.assertEqual('/mcp/tools/listPendingOutboundExportFilterOptions', client.calls[0][1])
+
+    def test_filter_options_rejects_missing_client_unknown_type_and_invalid_dependency(self):
+        with self.assertRaisesRegex(RuntimeError, 'api client is required'):
+            ListPendingOutboundExportFilterOptionsTool().call('country')
+
+        tool = ListPendingOutboundExportFilterOptionsTool(api_client=RecordingApiClient())
+        with self.assertRaisesRegex(ValueError, 'unsupported filter_type'):
+            tool.call('unknown')
+        with self.assertRaisesRegex(ValueError, 'product_type_id must be greater than 0'):
+            tool.call('product', product_type_id=0)
+        tool.call('country')
+
+    def test_export_requires_api_client(self):
+        with self.assertRaisesRegex(RuntimeError, 'api client is required'):
+            ExportPendingOutboundOrdersTool().call()
+
+    def test_gateways_register_both_tools_when_backend_enables_them(self):
+        local_names = {
+            item['name']
+            for item in GatewayApp(api_client=RecordingApiClient()).list_tools()
+        }
+        public_names = {
+            item['name']
+            for item in PublicGatewayApp(
+                PublicSessionStore(), PublicApiClient()
+            ).list_tools('GWS_test')
+        }
+
+        self.assertIn('export_pending_outbound_orders', local_names)
+        self.assertIn('list_pending_outbound_export_filter_options', local_names)
+        self.assertIn('export_pending_outbound_orders', public_names)
+        self.assertIn('list_pending_outbound_export_filter_options', public_names)
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 81 - 0
tools/export_pending_outbound_orders.py

@@ -0,0 +1,81 @@
+class ExportPendingOutboundOrdersTool:
+    name = 'export_pending_outbound_orders'
+    route_path = '/mcp/tools/exportPendingOutboundOrders'
+
+    FILTER_FIELDS = (
+        'number', 'product_type_id', 'product_id', 'order_warehouse_id',
+        'receiver_country', 'is_remove', 'address', 'inbound_date', 'status',
+        'is_battery', 'is_magnetic', 'is_wood', 'is_other', 'is_fda', 'is_toy',
+        'is_ultra_limit', 'is_sensitive', 'is_food', 'no_property',
+        'merge_declare_number', 'importer_id', 'packing_type',
+    )
+
+    def __init__(self, api_client=None):
+        self.api_client = api_client
+
+    def metadata(self):
+        properties = {
+            'number': {
+                'type': 'string',
+                'description': '单号:订单号、参考号;多个值用空格、英文逗号或回车分隔。',
+            },
+            'product_type_id': {'type': 'integer', 'description': '产品分类 ID。'},
+            'product_id': {
+                'type': 'array',
+                'items': {'type': 'integer'},
+                'description': '物流产品 ID 数组。先从筛选项工具获取授权值。',
+            },
+            'order_warehouse_id': {'type': 'integer', 'description': '集货仓库 ID。'},
+            'receiver_country': {'type': 'string', 'description': '目的国国家代码。'},
+            'is_remove': {
+                'type': 'integer',
+                'enum': [0, 1],
+                'description': '是否装柜剔除:1 是,0 否。',
+            },
+            'address': {'type': 'string', 'description': '派送地址模糊筛选。'},
+            'inbound_date': {
+                'type': 'string',
+                'description': '入库时间范围,格式 YYYY-MM-DD - YYYY-MM-DD。',
+            },
+            'status': {
+                'type': 'array',
+                'items': {'type': 'integer'},
+                'description': '订单状态整数数组。',
+            },
+            'merge_declare_number': {'type': 'string', 'description': '合并报关单号。'},
+            'importer_id': {'type': 'integer', 'description': '进口商 ID。'},
+            'packing_type': {
+                'type': 'string',
+                'description': '货物类型,多选值用英文逗号拼接。',
+            },
+        }
+        for field, label in (
+            ('is_battery', '带电'), ('is_magnetic', '带磁'), ('is_wood', '带木'),
+            ('is_other', '其它'), ('is_fda', 'FDA产品'), ('is_toy', '玩具'),
+            ('is_ultra_limit', '单箱尺寸超长超重'), ('is_sensitive', '敏感货'),
+            ('is_food', '食品'), ('no_property', '无属性'),
+        ):
+            properties[field] = {
+                'type': 'string',
+                'enum': ['Y'],
+                'description': '商品属性:{0},选中时传 Y。'.format(label),
+            }
+
+        return {
+            'name': self.name,
+            'description': (
+                '按 outbound/add.html 的筛选条件导出未排舱订单。筛选名称必须先通过 '
+                'list_pending_outbound_export_filter_options 获取当前员工有权限的 value。'
+            ),
+            'input_schema': {'type': 'object', 'properties': properties},
+        }
+
+    def call(self, request_id='rq_export_pending_outbound_orders', **kwargs):
+        if self.api_client is None:
+            raise RuntimeError('api client is required for export_pending_outbound_orders')
+
+        payload = {}
+        for field in self.FILTER_FIELDS:
+            if field in kwargs and kwargs[field] is not None:
+                payload[field] = kwargs[field]
+        return self.api_client.call_tool(self.name, self.route_path, payload, request_id)

+ 69 - 0
tools/list_pending_outbound_export_filter_options.py

@@ -0,0 +1,69 @@
+class ListPendingOutboundExportFilterOptionsTool:
+    name = 'list_pending_outbound_export_filter_options'
+    route_path = '/mcp/tools/listPendingOutboundExportFilterOptions'
+    FILTER_TYPES = (
+        'product_type', 'product', 'warehouse', 'country', 'importer',
+        'packing_type', 'order_status', 'goods_attribute', 'is_remove',
+    )
+
+    def __init__(self, api_client=None):
+        self.api_client = api_client
+
+    def metadata(self):
+        return {
+            'name': self.name,
+            'description': (
+                '列出当前员工有权限使用的未排舱订单导出筛选项。用户提供名称时,必须先查到 value,'
+                '不得猜测 ID;关键字搜索也只在授权结果内执行。'
+            ),
+            'input_schema': {
+                'type': 'object',
+                'properties': {
+                    'filter_type': {
+                        'type': 'string',
+                        'enum': list(self.FILTER_TYPES),
+                        'description': '筛选项类型。',
+                    },
+                    'product_type_id': {
+                        'type': 'integer',
+                        'description': '查询物流产品时可选的产品分类 ID。',
+                    },
+                    'keyword': {'type': 'string', 'description': '按名称或编码搜索。'},
+                    'page': {'type': 'integer', 'minimum': 1, 'default': 1},
+                    'limit': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 20},
+                },
+                'required': ['filter_type'],
+            },
+        }
+
+    def call(
+        self,
+        filter_type,
+        product_type_id=None,
+        keyword='',
+        page=1,
+        limit=20,
+        request_id='rq_list_pending_outbound_export_filter_options',
+    ):
+        if self.api_client is None:
+            raise RuntimeError(
+                'api client is required for list_pending_outbound_export_filter_options'
+            )
+
+        filter_type = str(filter_type).strip()
+        if filter_type not in self.FILTER_TYPES:
+            raise ValueError('unsupported filter_type')
+
+        payload = {
+            'filter_type': filter_type,
+            'keyword': str(keyword or '').strip(),
+            'page': max(1, int(page)),
+            'limit': max(1, min(100, int(limit))),
+        }
+        if product_type_id is not None:
+            product_type_id = int(product_type_id)
+            if product_type_id <= 0:
+                raise ValueError('product_type_id must be greater than 0')
+            payload['product_type_id'] = product_type_id
+
+        return self.api_client.call_tool(self.name, self.route_path, payload, request_id)