#!/usr/bin/env python # -*- coding: utf-8 -*- """ 实验流程表格生成脚本 - 读取传入的 experimentProcess JSON - 将数据转换为包含合并信息的脚本表格描述 - 输出格式与应用中的 scriptTable 占位符兼容 - 额外生成脚本图表(scriptChartX)需要的数据,便于测试 可通过环境变量定制行为: - TABLE_TOKEN:目标占位符(默认 scriptTable1) - TABLE_START_ROW:相对占位符的起始行偏移(整数,默认 2) - TABLE_START_COL:相对占位符的起始列偏移(整数,默认 0) - TABLE_INCLUDE_HEADERS:为 true/1/yes 时输出 headers 行 - CHART_TOKEN_PREFIX:脚本图表占位符前缀(默认 scriptChart) """ import json import os import sys from typing import Any, Dict, List, Tuple def _read_all_stdin() -> str: try: if sys.stdin and not sys.stdin.closed and not sys.stdin.isatty(): return sys.stdin.read() except Exception: pass return "" def _load_payload() -> Dict[str, Any]: raw = _read_all_stdin().strip() if not raw and len(sys.argv) > 1: arg = sys.argv[1] if os.path.exists(arg) and os.path.isfile(arg): with open(arg, "r", encoding="utf-8") as fh: raw = fh.read() else: raw = arg if not raw: raw = os.environ.get("EXPERIMENT_JSON", "").strip() if not raw: raw = "{}" data = json.loads(raw) if not isinstance(data, dict): raise ValueError("experiment JSON must be a dict") return data def _include_headers() -> bool: flag = os.environ.get("TABLE_INCLUDE_HEADERS", "").strip().lower() return flag in {"1", "true", "yes", "on"} def _parse_float(value: Any) -> float: try: if isinstance(value, (int, float)): return float(value) if isinstance(value, str): cleaned = value.replace("%", "").replace("°C", "").replace("RPM", "").strip() return float(cleaned) except Exception: pass return float("nan") def build_table_spec(exp: Dict[str, Any]) -> Dict[str, Any]: # 目标占位符,可通过环境变量覆盖,默认写入 {scriptTable1} token = os.environ.get("TABLE_TOKEN", "scriptTable1") # 当模板中的占位符位于表头时,默认向下偏移两行再开始写入 row_offset = int(os.environ.get("TABLE_START_ROW", "2") or 2) col_offset = int(os.environ.get("TABLE_START_COL", "0") or 0) headers: List[Any] = exp.get("headers") or [] rows: List[List[Any]] = exp.get("rows") or [] cells: List[Dict[str, Any]] = [] cursor_row = 0 if headers and _include_headers(): # 需要表头时,把 headers 行写成首行单元格 for ci, title in enumerate(headers): cells.append({"row": cursor_row, "col": ci, "value": title}) cursor_row += 1 idx = 0 while idx < len(rows): row = rows[idx] if isinstance(rows[idx], list) else [rows[idx]] label = str(row[0]) if row else "" span = 1 while idx + span < len(rows): nxt = rows[idx + span] if not isinstance(nxt, list): break if (len(nxt) > 0 and nxt[0] == label) and label != "": # 连续多行的首列内容相同时,记录合并行数 span += 1 else: break cells.append({ "row": cursor_row, "col": 0, "value": label, "rowspan": span, }) for offset in range(span): values = rows[idx + offset] if not isinstance(values, list): values = [values] # 从第二列开始依次写入实际数据,不需要列合并 for col_idx, val in enumerate(values[1:], start=1): cells.append({ "row": cursor_row + offset, "col": col_idx, "value": val, }) cursor_row += span idx += span return { "token": token, "startRow": row_offset, "startCol": col_offset, # cells 是最终交给应用的单元格描述列表 "cells": cells, } def build_chart_specs(exp: Dict[str, Any]) -> List[Dict[str, Any]]: """Return a fixed set of mock charts for testing. This ignores the experiment payload and always returns data for scriptChart1/2/3 so the template can render charts during development. """ prefix = os.environ.get("CHART_TOKEN_PREFIX", "scriptChart") # Simple step axis steps = [1, 2, 3, 4, 5, 6] charts: List[Dict[str, Any]] = [ { "token": f"{prefix}1", "title": "转速趋势(测试)", "xLabel": "步骤", "yLabel": "转速 (RPM)", "kind": "line", "grid": True, "series": [ {"label": "输入转速", "x": steps, "y": [900, 1200, 1500, 1800, 2000, 2200]}, {"label": "曲轴转速", "x": steps, "y": [150, 200, 260, 300, 330, 360]}, ], }, { "token": f"{prefix}2", "title": "流量分布(测试)", "xLabel": "步骤", "yLabel": "流量 (LPM)", "kind": "bar", "grid": True, "series": [ {"label": "流量", "x": steps, "y": [120, 140, 160, 180, 200, 220]}, ], }, { "token": f"{prefix}3", "title": "压力曲线(测试)", "xLabel": "步骤", "yLabel": "压力 (MPa)", "kind": "line", "grid": True, "series": [ {"label": "压力", "x": steps, "y": [8.0, 9.5, 10.2, 10.8, 11.3, 11.8]}, ], }, ] return charts def main() -> int: try: try: sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined] except Exception: pass payload = _load_payload() table_spec = build_table_spec(payload) chart_specs = build_chart_specs(payload) result = {"tables": [table_spec]} if chart_specs: result["charts"] = chart_specs print(json.dumps(result, ensure_ascii=False)) return 0 except Exception as exc: print(f"error: {exc}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())