iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
ChatGPT & Codex

ChatGPT + Codex 打造高效能 AI 開發工作流系列 第 25 篇

Day 25: 自動化排程工作流:結合 Make / n8n 與 OpenAI API 實現日常營運自動化

  • 分享至 

  • xImage
  •  

Day 25: 自動化排程工作流:結合 Make / n8n 與 OpenAI API 實現日常營運自動化 (n8n Make OpenAI Scheduling)

本日核心價值 (Core Focus): 用 n8n 或 Make 把「Cron → 呼叫 OpenAI → Slack/Email」做成可重跑的日常摘要工作流,密鑰放憑證存放區、每日 digest 做冪等,並保留一支 Python cron 作為不依賴 SaaS 的後備。

概念說明與實戰情境 (Overview)

Day 24 的 Git 腳本仍要人在終端機執行。營運向的工作——昨日 CI 失敗摘要、待辦票數、知識庫低分拒絕次數、成本 log 日切(Day 22)——適合排程,而不是開著筆電。n8n 與 Make.com 都提供 Cron、HTTP、Slack、Email 模組,本質相同:時間觸發、組 Prompt、打 OpenAI、把結構化結果送到頻道。差異在託管與計價,不在工作流形狀。上線前先定兩條規則:API Key 不寫進節點參數;同一天的 digest 重跑不得重複洗版。做不到這兩點,排程只是把帳單與通知事故自動化。

關鍵操作與範例 (Implementation & Example)

1. n8n 主路徑:Cron → HTTP OpenAI → Slack / Email

建議節點順序:

  1. Cron — 例如 0 9 * * 1-5(週一到週五 09:00,時區設團隊所在地)。
  2. Code 或 HTTP Request — 向內部 API 拉「昨日指標」JSON(失敗 job 數、Token 用量、RAG refuse 次數)。不要在 n8n 裡直接查生產資料庫。
  3. HTTP Request(OpenAI) — POST https://api.openai.com/v1/chat/completions(或團隊現用的 Responses 端點)。Authorization 來自 Credentials,不是節點裡的字串。
  4. IF — 解析 JSON;若模型回 no_changes: true,停止,不發 Slack。
  5. Slack 與/或 Email — 只送摘要欄位;完整 JSON 當附件或 log。

OpenAI 節點參數(高層示意,密鑰用 credential 參照):

{
  "nodes": [
    {
      "name": "Weekday 09:00",
      "type": "n8n-nodes-base.cron",
      "parameters": {
        "triggerTimes": {
          "item": [{ "hour": 9, "minute": 0 }]
        }
      }
    },
    {
      "name": "OpenAI Daily Digest",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "jsonBody": {
          "model": "gpt-4.1-mini",
          "temperature": 0,
          "messages": [
            {
              "role": "system",
              "content": "Summarize ops JSON into Traditional Chinese bullet points. Return JSON {title, bullets, no_changes}."
            },
            {
              "role": "user",
              "content": "={{ JSON.stringify($json) }}"
            }
          ]
        }
      }
    },
    {
      "name": "Slack Digest",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#eng-digest",
        "text": "={{ $json.choices[0].message.content }}"
      }
    }
  ]
}

實際 UI 裡 Header Auth 應綁「OpenAI API」credential;JSON 僅示意欄位名稱與資料流,貼上後仍需在編輯器對齊 n8n 版本的參數結構。Slack Bot Token 同樣走 Slack credential,不寫進 text 參數。OpenAI 節點應設 timeout(例如 30–60 秒)與有上限的重試:只對 429 / 5xx 重試,4xx 直接失敗並告警。Digest 的輸入 JSON 先由內部 API 聚合,模型只做「把已計算好的數字寫成可讀條列」,不要在排程裡臨時再打一遍所有 LLM 請求。

2. Make.com 對等模組

n8n Make.com 注意
Cron Schedule / Watch 時區與夏令時間;不要用瀏覽器本地時間假設
HTTP Request → OpenAI OpenAI / HTTP 連線放 Connections,不放 module 欄位
IF no_changes Filter / Router 無變更就結束 scenario
Slack Slack > Create a Message channel ID 用變數,勿寫死 webhook URL 在分享的 blueprint
Email Email / Gmail / Microsoft 365 收件人清單當團隊設定,不要讓模型指定任意外寄地址(Day 21 允許清單)

Make 的 scenario 匯出若含連線,分享前先換成空 Connections。模型輸出若包含「請改寄到某某信箱」,應用 Filter 擋下;收件人只來自 Make 設定。工作流失敗要另開告警(Email 或 Pager),不要把錯誤堆在同一則 digest:digest 沒送到,值班會以為「今天沒事」。OpenAI 與 Slack 節點都失敗時,冪等鍵不可標記為已送,隔天重跑才補得回。

3. 每日 digest 的冪等

Cron 會重試、手動 Execute Workflow 會再跑一次。冪等鍵用日期(團隊時區的 YYYY-MM-DD)+ workflow 名稱。第一次成功寫入資料表或 n8n Static Data / Make Data Store:digest:2026-08-17=sent。後續執行先查鍵,已存在就結束。不要用「訊息內容相同就不送」當唯一條件:內容可能因模型非確定性而微變,導致重複通知。溫度設 0、仍以日期鍵為準。

若摘要來源是 GitHub 或內部 API,拉取時帶 since=yesterday 00:00&until=today 00:00 的閉開區間,避免重跑把同一筆失敗 job 算兩次。這與 Day 22 的 per-request cost log 銜接:digest 讀「昨日加總」,不現場再打一遍所有 LLM 請求。時區必須寫死為團隊時區(本系列預設 Asia/Taipei),不要用 runner 的本地時間;否則週一早上可能吃到週日 UTC 的半日資料,冪等鍵也會錯日。指標建議固定欄位:failed_jobs、input_tokens_sum、usd_estimate_sum、rag_refuse_count、open_tickets,缺值填 0,方便模型與 IF 節點穩定解析。

4. Python 後備:系統 cron / Task Scheduler

SaaS 配額用盡、n8n 升級失敗時,同一份契約應能在本機或既有 runner 跑。

from __future__ import annotations

import hashlib
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path

from openai import OpenAI

STATE = Path(os.environ.get("DIGEST_STATE", ".digest-state.json"))
TZ_OFFSET_HOURS = 8  # Asia/Taipei without extra tzdata dependency


def today_key() -> str:
    local = datetime.now(timezone.utc) + timedelta(hours=TZ_OFFSET_HOURS)
    return local.date().isoformat()


def already_sent(day: str) -> bool:
    if not STATE.exists():
        return False
    data = json.loads(STATE.read_text(encoding="utf-8"))
    return data.get("last_sent_day") == day


def mark_sent(day: str, fingerprint: str) -> None:
    STATE.write_text(
        json.dumps({"last_sent_day": day, "fingerprint": fingerprint}, ensure_ascii=False),
        encoding="utf-8",
    )


def load_metrics() -> dict:
    path = Path(os.environ["METRICS_JSON"])
    return json.loads(path.read_text(encoding="utf-8"))


def main() -> None:
    day = today_key()
    if already_sent(day):
        print(f"idempotent skip: {day}")
        return
    metrics = load_metrics()
    client = OpenAI()
    resp = client.chat.completions.create(
        model=os.environ.get("CHAT_MODEL", "gpt-4.1-mini"),
        temperature=0,
        messages=[
            {
                "role": "system",
                "content": (
                    "Summarize ops metrics as Traditional Chinese bullets. "
                    "Return JSON with keys title, bullets, no_changes."
                ),
            },
            {"role": "user", "content": json.dumps(metrics, ensure_ascii=False)},
        ],
    )
    content = resp.choices[0].message.content or "{}"
    parsed = json.loads(content)
    if parsed.get("no_changes"):
        mark_sent(day, "no_changes")
        print("no changes; marked sent")
        return
    webhook = os.environ["SLACK_WEBHOOK_URL"]
    import urllib.request

    body = json.dumps({"text": parsed.get("title", "digest") + "\n" + "\n".join(parsed.get("bullets") or [])}).encode()
    req = urllib.request.Request(webhook, data=body, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=30)
    mark_sent(day, hashlib.sha256(content.encode()).hexdigest())
    print(f"sent digest for {day}")


if __name__ == "__main__":
    main()

Linux / WSL:crontab 寫 0 9 * * 1-5 cd /path && . .venv/bin/activate && python digest.py。Windows:工作排程器呼叫 WSL 或 Git Bash。OPENAI_API_KEY、SLACK_WEBHOOK_URL 放環境變數或 secret store,不放進 repo。Webhook 僅用於後備腳本的單一頻道;n8n 路徑優先用 Slack credential。後備腳本與 n8n 必須共用同一份冪等狀態(資料表或網路磁碟上的 state 檔),否則兩條排程會同一天各送一次。切換主路徑時先停掉另一條,再清當天鍵,避免補送變成重送。

注意事項與常見失敗 (Pitfalls)

  • 把 Bearer token 貼在 HTTP 節點的 Header 欄位,blueprint 一匯出就外洩。修法:n8n Credentials / Make Connections;匯出前確認 JSON 無 secret。
  • Cron 重試導致同一天三則 Slack。修法:以日期為冪等鍵;成功才 mark;失敗不 mark。
  • 讓模型決定收件人或頻道。修法:頻道與 Email 允許清單寫死在工作流;模型只產出標題與 bullets(Day 21)。
  • 在排程裡同步打旗艦模型重跑所有原始請求。修法:digest 讀已寫入的日切指標與 cost log(Day 22)。
  • n8n 與 Python 後備各跑一次。修法:共用同一個 state(資料表或檔案路徑),或明確切換「只啟用一條排程」。
  • Prompt 把內部指標 HTML 整段當 system。修法:指標當 user JSON;system 只放摘要政策。

本日總結 (Takeaways)

  • 營運摘要的標準形狀是 Cron → OpenAI HTTP → Slack/Email;n8n 與 Make 模組一一對應。
  • 密鑰只存在 credential / 環境變數,不存在節點 JSON。
  • 每日 digest 用日期冪等,避免重跑洗版。
  • 保留 Python cron 後備,SaaS 不可用時契約仍能執行。

明日預告 (Next)

排程會把「慢」與「漏」變成每天的噪音。下一步回到執行期本身:Day 26 將做系統效能調優:利用 ChatGPT 快速發現 Memory Leak 與 I/O 瓶頸。


上一篇
Day 24: Git 自動化工作流:自動生成 Commit Message 與 Release Notes
下一篇
Day 26: 系統效能調優:利用 ChatGPT 快速發現 Memory Leak 與 I/O 瓶頸
系列文
ChatGPT + Codex 打造高效能 AI 開發工作流 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言