gateway_queries.py 4.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """网关查询共用的窗口校验、服务端身份和可信文件归档。"""
  2. import asyncio
  3. from datetime import datetime, timedelta
  4. from pydantic import BaseModel, ConfigDict, Field
  5. import config
  6. from algorithms.main_fan.analysis import LOCAL_ZONE, local_time
  7. from db import db_conn
  8. from extensions import get_service
  9. from extensions.main_fan import exploration
  10. from extensions.registry import load_extensions
  11. from runtime.python_sandbox import digest, workspace_file
  12. class CurveQuery(BaseModel):
  13. model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
  14. node_id: str = Field(min_length=1, max_length=128, pattern=r'^[A-Za-z0-9_-]+$')
  15. dept_id: str = Field(min_length=1, max_length=64, pattern=r'^[A-Za-z0-9_-]+$')
  16. start_time: str = Field(min_length=1, max_length=64)
  17. end_time: str = Field(min_length=1, max_length=64)
  18. def parse_time(value):
  19. if not isinstance(value, str) or not ('T' in value or ' ' in value):
  20. raise ValueError('时间须包含日期和时分秒,例如 2026-10-19 11:00:00 或带时区的 ISO 时间')
  21. try:
  22. return local_time(datetime.fromisoformat(value))
  23. except ValueError as exc:
  24. raise ValueError('无法解析时间,请使用日期和时分秒,可附带时区') from exc
  25. def curve_parameters(node_id, dept_id, start_time, end_time, *, node_key='nodeIds'):
  26. query = CurveQuery(node_id=node_id, dept_id=dept_id, start_time=start_time, end_time=end_time)
  27. start, end = parse_time(query.start_time), parse_time(query.end_time)
  28. if start.microsecond or end.microsecond:
  29. raise ValueError('查询起止时间须精确到秒,不支持小数秒')
  30. if start >= end:
  31. raise ValueError('查询结束时间必须晚于开始时间')
  32. maximum = config.get_int('MAIN_FAN_SERIES_MAX_HOURS', 24)
  33. if maximum <= 0 or end - start > timedelta(hours=maximum):
  34. raise ValueError(f'单次查询窗口最多 {maximum} 小时,请分段查询')
  35. return {node_key: query.node_id, 'deptId': query.dept_id,
  36. 'startTime': start.strftime('%Y-%m-%d %H:%M:%S'),
  37. 'endTime': end.strftime('%Y-%m-%d %H:%M:%S')}, start, end
  38. async def fetch(identifier, user, session, path, params):
  39. workflow = await asyncio.to_thread(exploration._load, identifier, user, session)
  40. await asyncio.to_thread(exploration.validate_inputs, workflow, user, session)
  41. bound_dept = workflow['context']['task_input'].get('dept_id')
  42. actual_dept = str(dict(params).get('deptId', ''))
  43. if bound_dept and actual_dept != bound_dept:
  44. raise ValueError('查询部门不属于本次已绑定煤矿')
  45. if len(workflow['stages']) >= 128:
  46. raise ValueError('探索操作已达上限,请先提交已有结果')
  47. if not config.get_bool('SHANXI_GATEWAY_ENABLED', False):
  48. raise ValueError('取数需要启用 SHANXI_GATEWAY_ENABLED 并配置网关地址和凭证')
  49. try:
  50. gateway = get_service('shaanxi_gateway')
  51. except RuntimeError as exc:
  52. raise ValueError('取数网关尚未启动,请在启用网关的 RuntimeEnvironment 中调用') from exc
  53. spec = load_extensions().business('main_fan')
  54. body = await gateway.proxy(spec.gateway.agent_code, path, params=params)
  55. request = {'method': 'GET', 'path': path, 'params': params,
  56. 'agent_code': spec.gateway.agent_code, 'fetched_at': datetime.now(LOCAL_ZONE).isoformat()}
  57. return workflow, request, body
  58. def archive(workflow, user, session, request, body, data, summary, *, kind, collection, operation, filename):
  59. exploration.validate_inputs(workflow, user, session)
  60. query_id = db_conn.new_id()
  61. prefix = f"/workspace/exploration/{workflow['id']}/queries/{query_id}"
  62. paths = {'request_path': prefix + '/request.json', 'raw_path': prefix + '/response.json',
  63. 'data_path': prefix + '/' + filename}
  64. data = {'schema_version': 1, 'query_id': query_id, 'query': request, **data}
  65. exports = {}
  66. for key, value in (('request_path', request), ('raw_path', body), ('data_path', data)):
  67. path = workspace_file(user, session, paths[key])
  68. exploration._write(path, value)
  69. exports[paths[key]] = digest(path)
  70. params = dict(request['params'])
  71. receipt = {'ok': True, 'query_id': query_id, 'kind': kind, 'dept_id': params['deptId'],
  72. **summary, **paths,
  73. 'python_paths': {key.removesuffix('_path'): value.removeprefix('/workspace/')
  74. for key, value in paths.items()}, 'sha256': exports[paths['data_path']]}
  75. if 'startTime' in params:
  76. receipt.update(node_id=params.get('nodeId', params.get('nodeIds')), requested_range={
  77. 'start_time': params['startTime'], 'end_time': params['endTime'], 'timezone': 'Asia/Shanghai'})
  78. context = workflow['context']
  79. context.setdefault(collection, []).append(receipt)
  80. context.setdefault('query_exports', {}).update(exports)
  81. exploration._persist(workflow, user, session, operation, receipt)
  82. return receipt