{
  "name": "Runner\u53ef\u89c2\u6d4b\u6027-\u7edf\u4e00",
  "nodes": [
    {
      "parameters": {},
      "id": "manual-trigger",
      "name": "\u624b\u52a8\u89e6\u53d1",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        220,
        200
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "\u5b9a\u65f6\u89e6\u53d1\u5668",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        220,
        400
      ]
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "triggerSource",
              "value": "runner-observability-manual"
            }
          ]
        }
      },
      "id": "set-manual-source",
      "name": "\u8bbe\u7f6e\u6765\u6e90-\u624b\u52a8",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.2,
      "position": [
        460,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "const dtf = new Intl.DateTimeFormat('en-GB', {\n  timeZone: 'Asia/Shanghai',\n  hour12: false,\n  weekday: 'short',\n  year: 'numeric',\n  month: '2-digit',\n  day: '2-digit',\n  hour: '2-digit',\n  minute: '2-digit',\n  second: '2-digit',\n});\nconst parts = dtf.formatToParts(new Date());\nconst map = Object.fromEntries(parts.map(p => [p.type, p.value]));\nconst weekdayMap = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };\nconst weekday = weekdayMap[map.weekday] ?? 0;\nconst hour = Number(map.hour);\nconst minute = Number(map.minute);\nconst minutes = hour * 60 + minute;\nconst isWeekday = weekday >= 1 && weekday <= 5;\nconst inMorning = minutes >= 570 && minutes <= 690;\nconst inAfternoon = minutes >= 780 && minutes <= 900;\nconst inTradingTime = isWeekday && (inMorning || inAfternoon);\nconst now = `${map.year}-${map.month}-${map.day}T${map.hour}:${map.minute}:${map.second}+08:00`;\nreturn [{ now, isWeekday, inTradingTime }];\n"
      },
      "id": "calc-trading-time",
      "name": "\u8ba1\u7b97\u4ea4\u6613\u65f6\u6bb5",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.inTradingTime }}",
              "operation": "equals",
              "value2": true
            }
          ]
        }
      },
      "id": "if-trading-time",
      "name": "\u662f\u5426\u4ea4\u6613\u65f6\u6bb5",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        700,
        400
      ]
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "triggerSource",
              "value": "runner-observability-intraday"
            }
          ]
        }
      },
      "id": "set-intraday-source",
      "name": "\u8bbe\u7f6e\u6765\u6e90-\u76d8\u4e2d",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.2,
      "position": [
        940,
        320
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:8787/api/runner/run",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "plan",
              "value": "m0m1"
            },
            {
              "name": "triggerType",
              "value": "n8n"
            },
            {
              "name": "triggerSource",
              "value": "={{ $json.triggerSource || 'runner-observability' }}"
            },
            {
              "name": "steps",
              "value": "m0-AB"
            }
          ]
        },
        "options": {}
      },
      "id": "execute-runner",
      "name": "\u6267\u884cRunner",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        1180,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const obj = $json;\nif (!obj || !obj.runId) throw new Error('Runner response invalid: ' + JSON.stringify(obj));\nconst root = '/Users/una5577/Documents/trae_projects/a-stock-monitor/';\nreturn [{\n  ...obj,\n  journalAbs: root + (obj.runJournalPath || obj.journal),\n  qaAbs: obj.qa ? (root + obj.qa) : null,\n}];\n"
      },
      "id": "parse-stdout",
      "name": "\u89e3\u6790stdout",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1420,
        260
      ]
    },
    {
      "parameters": {
        "filePath": "={{ $json.journalAbs }}"
      },
      "id": "read-journal",
      "name": "\u8bfb\u53d6RunJournal",
      "type": "n8n-nodes-base.readBinaryFile",
      "typeVersion": 1,
      "position": [
        1660,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const b = $binary?.data?.data;\nif (!b) throw new Error('journal binary is empty');\nconst txt = Buffer.from(b, 'base64').toString('utf-8');\nconst journal = JSON.parse(txt);\nconst status = journal.status;\nreturn [{ journal, status }];\n"
      },
      "id": "decode-journal",
      "name": "\u89e3\u7801RunJournal",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1900,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const journal = $json.journal;\nconst stdout = $node[\"\u89e3\u6790stdout\"].json;\nconst steps = (journal.steps || []).map((s) => {\n  const providers = (s.providers || []).map((p) => ({\n    datasetId: p.datasetId ?? p.dataset ?? null,\n    providerId: p.providerId ?? null,\n    asOf: p.asOf ?? null,\n  }));\n  const outputs = (s.outputs || []).map((o) => ({\n    type: o.type ?? null,\n    path: o.path ?? null,\n  }));\n  const warnings = (s.warnings || []).map((w) => ({\n    code: w.code ?? null,\n    severity: w.severity ?? null,\n    message: w.message ?? null,\n    paths: w.paths ?? [],\n  }));\n  return {\n    step: s.name,\n    status: s.status,\n    providers,\n    outputs,\n    error: s.error ?? null,\n    warnings,\n  };\n});\nconst errors = [];\nfor (const s of steps) {\n  for (const w of (s.warnings || [])) {\n    if (w.severity === 'error') errors.push({ step: s.step, code: w.code, message: w.message });\n  }\n  if (s.status === 'failed' && s.error) errors.push({ step: s.step, code: 'STEP_FAILED', message: s.error.message ?? String(s.error) });\n}\nconst warningsCount = steps.reduce((n, s) => n + (s.warnings || []).filter((w) => w.severity === 'warn').length, 0);\nconst errorsCount = errors.length;\nreturn [{\n  runId: journal.runId ?? stdout.runId ?? null,\n  status: journal.status ?? stdout.status ?? null,\n  day: journal.day ?? stdout.day ?? null,\n  asOf: journal.asOf ?? stdout.asOf ?? null,\n  triggerSource: journal.trigger?.source ?? stdout.triggerSource ?? null,\n  runJournalPath: stdout.journal ?? stdout.runJournalPath ?? null,\n  warningsCount,\n  errorsCount,\n  steps,\n  errors,\n}];\n"
      },
      "id": "extract-acceptance",
      "name": "\u63d0\u53d6\u9a8c\u6536\u6458\u8981",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2140,
        120
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.status }}",
              "operation": "equals",
              "value2": "failed"
            }
          ]
        }
      },
      "id": "if-failed",
      "name": "\u662f\u5426\u5931\u8d25",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        2380,
        120
      ]
    },
    {
      "parameters": {
        "fileName": "logs/runner-observe-errors.log",
        "operation": "append",
        "fileContent": "={{ $now.setZone('Asia/Shanghai').toFormat('yyyy-LL-dd HH:mm:ss') }} - runner failed\\n{{ JSON.stringify($json.journal, null, 2) }}\\n\\n"
      },
      "id": "log-failed",
      "name": "\u8bb0\u5f55\u5931\u8d25\u65e5\u5fd7",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [
        2380,
        200
      ]
    },
    {
      "parameters": {},
      "id": "noop",
      "name": "\u7ed3\u675f",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2380,
        320
      ]
    }
  ],
  "connections": {
    "\u624b\u52a8\u89e6\u53d1": {
      "main": [
        [
          {
            "node": "\u8bbe\u7f6e\u6765\u6e90-\u624b\u52a8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u5b9a\u65f6\u89e6\u53d1\u5668": {
      "main": [
        [
          {
            "node": "\u8ba1\u7b97\u4ea4\u6613\u65f6\u6bb5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u8bbe\u7f6e\u6765\u6e90-\u624b\u52a8": {
      "main": [
        [
          {
            "node": "\u6267\u884cRunner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u8ba1\u7b97\u4ea4\u6613\u65f6\u6bb5": {
      "main": [
        [
          {
            "node": "\u662f\u5426\u4ea4\u6613\u65f6\u6bb5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u662f\u5426\u4ea4\u6613\u65f6\u6bb5": {
      "main": [
        [
          {
            "node": "\u8bbe\u7f6e\u6765\u6e90-\u76d8\u4e2d",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\u7ed3\u675f",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u8bbe\u7f6e\u6765\u6e90-\u76d8\u4e2d": {
      "main": [
        [
          {
            "node": "\u6267\u884cRunner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u6267\u884cRunner": {
      "main": [
        [
          {
            "node": "\u89e3\u6790stdout",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u89e3\u6790stdout": {
      "main": [
        [
          {
            "node": "\u8bfb\u53d6RunJournal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u8bfb\u53d6RunJournal": {
      "main": [
        [
          {
            "node": "\u89e3\u7801RunJournal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u89e3\u7801RunJournal": {
      "main": [
        [
          {
            "node": "\u63d0\u53d6\u9a8c\u6536\u6458\u8981",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u63d0\u53d6\u9a8c\u6536\u6458\u8981": {
      "main": [
        [
          {
            "node": "\u662f\u5426\u5931\u8d25",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u662f\u5426\u5931\u8d25": {
      "main": [
        [
          {
            "node": "\u8bb0\u5f55\u5931\u8d25\u65e5\u5fd7",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\u7ed3\u675f",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u8bb0\u5f55\u5931\u8d25\u65e5\u5fd7": {
      "main": [
        [
          {
            "node": "\u7ed3\u675f",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {},
  "versionId": "runner-observability-unified-v1"
}