|
|
@@ -1,5 +1,6 @@
|
|
|
import json
|
|
|
import logging
|
|
|
+from urllib.parse import urlparse
|
|
|
|
|
|
from constants import DEVICE_INVALID_MESSAGE
|
|
|
|
|
|
@@ -26,9 +27,10 @@ class OutputPresenter:
|
|
|
'export_pending_outbound_orders',
|
|
|
'export_out_of_province_port_data',
|
|
|
))
|
|
|
+ TASK_TOOLS = frozenset(('query_export_task',))
|
|
|
SAFE_TOOLS = (
|
|
|
TABLE_TOOLS | DETAIL_TOOLS | ORDER_DETAIL_TOOLS
|
|
|
- | OPTION_TOOLS | EXPORT_TOOLS
|
|
|
+ | OPTION_TOOLS | EXPORT_TOOLS | TASK_TOOLS
|
|
|
)
|
|
|
|
|
|
ORDER_DETAIL_SECTIONS = {
|
|
|
@@ -417,6 +419,9 @@ class OutputPresenter:
|
|
|
'so_numbers': 'SO号',
|
|
|
'file_type': '资料类型',
|
|
|
},
|
|
|
+ 'query_export_task': {
|
|
|
+ 'task_ref': '导出任务引用',
|
|
|
+ },
|
|
|
}
|
|
|
|
|
|
ERROR_MESSAGES = {
|
|
|
@@ -487,7 +492,9 @@ class OutputPresenter:
|
|
|
)
|
|
|
if tool_name in self.OPTION_TOOLS:
|
|
|
return self._present_options(data, tool_result.get('meta'), meta)
|
|
|
- return self._present_export(data, meta)
|
|
|
+ if tool_name in self.EXPORT_TOOLS:
|
|
|
+ return self._present_export_submission(data, meta)
|
|
|
+ return self._present_export_task(data, meta)
|
|
|
|
|
|
def _present_order_detail(self, data, raw_meta, meta):
|
|
|
if set(data) != {'section', 'order_number', 'payload'}:
|
|
|
@@ -910,17 +917,120 @@ class OutputPresenter:
|
|
|
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():
|
|
|
+ def _present_export_submission(self, data, meta):
|
|
|
+ expected = {'task_ref', 'status', 'retry_after_seconds'}
|
|
|
+ if set(data) != expected:
|
|
|
+ return self._format_error(meta)
|
|
|
+ task_ref = data.get('task_ref')
|
|
|
+ retry_after = data.get('retry_after_seconds')
|
|
|
+ if (
|
|
|
+ not isinstance(task_ref, str) or not task_ref.strip()
|
|
|
+ or data.get('status') != 'queued'
|
|
|
+ or isinstance(retry_after, bool)
|
|
|
+ or not isinstance(retry_after, int)
|
|
|
+ or retry_after <= 0
|
|
|
+ ):
|
|
|
return self._format_error(meta)
|
|
|
+ task = {
|
|
|
+ 'task_ref': task_ref.strip(),
|
|
|
+ 'status': 'queued',
|
|
|
+ 'retry_after_seconds': retry_after,
|
|
|
+ }
|
|
|
content = {
|
|
|
- 'message': '文件已生成',
|
|
|
- 'files': [{'label': '导出文件', 'url': url.strip()}],
|
|
|
+ 'message': '导出任务已提交',
|
|
|
+ 'task': task,
|
|
|
}
|
|
|
- text = '文件已生成\n- 导出文件: {0}'.format(url.strip())
|
|
|
+ text = (
|
|
|
+ '导出任务已提交\n'
|
|
|
+ '- 任务引用: {0}\n'
|
|
|
+ '- 建议 {1} 秒后单独查询任务状态'
|
|
|
+ ).format(task['task_ref'], retry_after)
|
|
|
return self._success_result(content, text, meta)
|
|
|
|
|
|
+ def _present_export_task(self, data, meta):
|
|
|
+ task_ref = data.get('task_ref')
|
|
|
+ status = data.get('status')
|
|
|
+ if (
|
|
|
+ not isinstance(task_ref, str)
|
|
|
+ or not task_ref.strip()
|
|
|
+ or status not in ('queued', 'running', 'completed', 'failed')
|
|
|
+ ):
|
|
|
+ return self._format_error(meta)
|
|
|
+
|
|
|
+ task = {'task_ref': task_ref.strip(), 'status': status}
|
|
|
+ if status in ('queued', 'running'):
|
|
|
+ if set(data) != {'task_ref', 'status', 'retry_after_seconds'}:
|
|
|
+ return self._format_error(meta)
|
|
|
+ retry_after = data.get('retry_after_seconds')
|
|
|
+ if (
|
|
|
+ isinstance(retry_after, bool)
|
|
|
+ or not isinstance(retry_after, int)
|
|
|
+ or retry_after <= 0
|
|
|
+ ):
|
|
|
+ return self._format_error(meta)
|
|
|
+ task['retry_after_seconds'] = retry_after
|
|
|
+ label = '等待中' if status == 'queued' else '生成中'
|
|
|
+ content = {'message': '导出任务{0}'.format(label), 'task': task}
|
|
|
+ text = (
|
|
|
+ '导出任务{0}\n- 任务引用: {1}\n- 建议 {2} 秒后再次查询'
|
|
|
+ ).format(label, task['task_ref'], retry_after)
|
|
|
+ return self._success_result(content, text, meta)
|
|
|
+
|
|
|
+ if status == 'failed':
|
|
|
+ if set(data) != {'task_ref', 'status'}:
|
|
|
+ return self._format_error(meta)
|
|
|
+ content = {'message': '导出任务失败,请重新提交', 'task': task}
|
|
|
+ text = '导出任务失败,请重新提交'
|
|
|
+ return self._success_result(content, text, meta)
|
|
|
+
|
|
|
+ if set(data) != {'task_ref', 'status', 'files'}:
|
|
|
+ return self._format_error(meta)
|
|
|
+ files = data.get('files')
|
|
|
+ if not isinstance(files, list) or not files:
|
|
|
+ return self._format_error(meta)
|
|
|
+ safe_files = []
|
|
|
+ for item in files:
|
|
|
+ if not isinstance(item, dict) or set(item) != {'label', 'url'}:
|
|
|
+ return self._format_error(meta)
|
|
|
+ label = item.get('label')
|
|
|
+ url = item.get('url')
|
|
|
+ if (
|
|
|
+ not isinstance(label, str) or not label.strip()
|
|
|
+ or not self._valid_http_url(url)
|
|
|
+ ):
|
|
|
+ return self._format_error(meta)
|
|
|
+ safe_files.append({'label': label.strip(), 'url': url.strip()})
|
|
|
+ content = {
|
|
|
+ 'message': '文件已生成',
|
|
|
+ 'task': task,
|
|
|
+ 'files': safe_files,
|
|
|
+ }
|
|
|
+ lines = ['文件已生成']
|
|
|
+ for item in safe_files:
|
|
|
+ lines.append('- {0}: {1}'.format(item['label'], item['url']))
|
|
|
+ return self._success_result(content, '\n'.join(lines), meta)
|
|
|
+
|
|
|
+ def _valid_http_url(self, value):
|
|
|
+ if not isinstance(value, str) or not value.strip():
|
|
|
+ return False
|
|
|
+ if value != value.strip() or '\\' in value or any(
|
|
|
+ character.isspace() or ord(character) < 32 or ord(character) == 127
|
|
|
+ for character in value
|
|
|
+ ):
|
|
|
+ return False
|
|
|
+ try:
|
|
|
+ parsed = urlparse(value)
|
|
|
+ port = parsed.port
|
|
|
+ return (
|
|
|
+ parsed.scheme in ('http', 'https')
|
|
|
+ and bool(parsed.hostname)
|
|
|
+ and parsed.username is None
|
|
|
+ and parsed.password is None
|
|
|
+ and (port is None or 0 < port <= 65535)
|
|
|
+ )
|
|
|
+ except ValueError:
|
|
|
+ return False
|
|
|
+
|
|
|
def _business_error(self, tool_name, code, raw_message, meta):
|
|
|
if code not in self.ERROR_MESSAGES and code != 'MCP_1401':
|
|
|
logger.warning(
|