第 6 章

設定自動排程

用時間觸發器每 15 分鐘檢查一次,把到期的帖自動發佈,並讓長期權杖自動續期。

到這裡,發帖仍然要你自己按。這一章讓它自己跑。

關鍵在於 Apps Script 的時間觸發器:它在 Google 的伺服器上執行,不是在你的電腦上。所以你關掉瀏覽器、關掉電腦、去旅行,它照樣運作。

狀態流轉

Draft ──(你填時間並改成 Scheduled)──▶ Scheduled
                                          │ 時間到

                                   Posted 或 Error

排程器只碰 Scheduled 而且時間已到的行。Draft 是安全的暫存區——寫到一半的帖不會意外出街。

1. 加入排程程式碼

Code.gs 底部加入:

/* ========================= 排程 ========================= */

var SCHED_MINUTES = 15;
var REFRESH_AFTER_DAYS = 40; // 長期權杖約 60 天到期,提早續

/**
 * 觸發器的目標函式:發佈所有已到期的 Scheduled 行。
 * 每一行獨立處理 —— 一行失敗不會擋住其他行。
 */
function runScheduler() {
  refreshTokenIfNeeded_();

  var sh = sheet_();
  var data = sh.getDataRange().getValues();
  var now = new Date();
  var posted = 0;

  for (var r = 1; r < data.length; r++) {
    var row = data[r];
    if (String(row[COL.STATUS] || "").trim().toLowerCase() !== "scheduled") continue;

    var when = row[COL.WHEN];
    if (!(when instanceof Date) || when > now) continue;

    var text = String(row[COL.TEXT] || "").trim();
    if (!text) continue;

    try {
      var res = publishRow_(text, String(row[COL.LINK] || "").trim());
      sh.getRange(r + 1, COL.STATUS + 1).setValue("Posted");
      sh.getRange(r + 1, COL.POSTED + 1).setValue(new Date());
      sh.getRange(r + 1, COL.URL + 1).setValue(res.url);
      posted++;
    } catch (err) {
      // 錯誤寫回試算表,否則它會靜靜地失敗而你永遠不知道。
      sh.getRange(r + 1, COL.STATUS + 1).setValue("Error");
      sh.getRange(r + 1, COL.URL + 1).setValue(String(err.message).slice(0, 300));
    }
  }
  return posted;
}

/** 安裝(或重裝)每 15 分鐘的觸發器。重複執行是安全的。 */
function installScheduler() {
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === "runScheduler") ScriptApp.deleteTrigger(t);
  });
  ScriptApp.newTrigger("runScheduler")
    .timeBased()
    .everyMinutes(SCHED_MINUTES)
    .create();
  toast_("已安裝自動排程(每 " + SCHED_MINUTES + " 分鐘檢查一次)。");
}

function runSchedulerNow() {
  toast_("排程器已執行,發佈了 " + runScheduler() + " 條。");
}

/* ====================== 權杖自動續期 ====================== */

/** 權杖超過 REFRESH_AFTER_DAYS 天就續期,避免 60 天後失效。 */
function refreshTokenIfNeeded_() {
  var p = props_();
  var at = Number(p.getProperty("TOKEN_AT") || 0);
  if (at && (Date.now() - at) / 86400000 < REFRESH_AFTER_DAYS) return;

  try {
    var res = UrlFetchApp.fetch(
      "https://graph.threads.net/refresh_access_token" +
        "?grant_type=th_refresh_token&access_token=" +
        encodeURIComponent(token_()),
      { muteHttpExceptions: true }
    );
    var data = JSON.parse(res.getContentText() || "{}");
    if (data.access_token) {
      p.setProperty("ACCESS_TOKEN", data.access_token);
      p.setProperty("TOKEN_AT", String(Date.now()));
      Logger.log("權杖已續期。");
    }
  } catch (err) {
    Logger.log("權杖續期失敗:" + err.message);
  }
}

2. 更新選單

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("Threads")
    .addItem("連接 Threads", "connectThreads")
    .addItem("測試連線", "testConnection")
    .addSeparator()
    .addItem("立即發佈選取的一行", "postSelectedRow")
    .addItem("立即執行排程器", "runSchedulerNow")
    .addSeparator()
    .addItem("安裝自動排程", "installScheduler")
    .addToUi();
}

3. 安裝觸發器

儲存 → 回試算表重新整理 → Threads → 安裝自動排程

首次會要求授權(這次是「代你執行」的權限)。授權後,左側 ⏰ 觸發條件 應該會看到一條 runScheduler,每 15 分鐘執行。

Apps Script 觸發條件設定:runScheduler、時間驅動、分鐘計時器、每 15 分鐘

這一步做不到,整套系統就不會自動運作。 很多人寫完程式碼卻忘了安裝觸發器,然後以為壞了。

4. ⚠️ 時區

Publish At 用的是試算表的時區,而排程器比較時間用的是指令碼專案的時區。兩者不一致就會早出或遲出。

  • 試算表:檔案 → 設定 → 時區
  • 指令碼:Apps Script 專案設定 → 時區

確保兩者相同(例如都設為香港時間)。

5. 試一次

Calendar 填一行:

StatusPublish AtText
Scheduled(現在之後 2 分鐘)排程測試。

然後選 Threads → 立即執行排程器,不用等。它應該立刻說發佈了 0 條(還沒到時間)。等過了那個時間再執行一次,就會發佈。

確認手動執行沒問題後,就交給觸發器。

觸發器是近似的:每 15 分鐘檢查一次,所以實際發佈時間可能比 Publish At 遲最多 15 分鐘。要更準就把 SCHED_MINUTES 調細,但沒必要——社交平台的發文時間本來就不需要精準到分鐘。

6. 日常用法

由現在開始,你的流程是:

  1. 平時把想到的內容寫進 Calendar,StatusDraft
  2. 想出街的,填上 Publish At,Status 改成 Scheduled
  3. 收工

這一章完成了什麼

  • ✅ 每 15 分鐘自動檢查並發佈
  • ✅ 錯誤寫回試算表,不會靜靜失敗
  • ✅ 權杖自動續期,不會 60 天後突然停止
  • ✅ 全程在 Google 伺服器上執行,不需要開電腦

最後一章:把成效數據抓回來,以及一份疑難排解清單。

步驟最後驗證於 2026年7月20日