實戰範例 003:跨平台 (Twitter & B站) 技術週報自動排程與發佈系統
簡介與場景
技術團隊或開源社群經常需要將每週的進展、技術文章或 Release Notes 同步發佈到多個平台。手動複製貼上不僅耗時,還容易出錯。 本範例將帶領讀者實作一個自動化的「技術週報廣播器 (Tech Weekly Broadcaster)」,目標平台包含 Twitter (X) 與 Bilibili (B站)。 這類跨平台發佈系統最大的痛點在於:不同平台對於 API 的頻率限制、多媒體檔案 (如圖片) 的上傳格式、以及 OAuth2.0 的 Token 更新機制各不相同。我們將利用 Agent Reach 容錯設計模式,解決授權過期與網路抖動帶來的問題。
原始代碼:過度設計或脆弱連線
在未經深思熟慮的情況下,開發者可能會寫出線性執行的腳本。一旦某個平台發佈失敗,後續的平台發佈也會被阻斷。
const twitterClient = require('twitter-api-client');
const bilibiliClient = require('bilibili-api-sdk');
const fs = require('fs');
async function broadcastWeeklyReport(reportText, imagePath) {
// 讀取圖片
const imageBuffer = fs.readFileSync(imagePath);
// 1. 發佈到 Twitter
console.log("發佈至 Twitter...");
const twtImage = await twitterClient.uploadMedia(imageBuffer);
await twitterClient.postTweet({ text: reportText, media_id: twtImage.id });
console.log("Twitter 發佈成功");
// 2. 發佈到 B 站動態
// 如果 Twitter 發佈失敗拋出異常,這裡永遠不會執行
console.log("發佈至 B 站...");
const biliImage = await bilibiliClient.uploadImage(imageBuffer);
await bilibiliClient.postDynamic({ content: reportText, pictures: [biliImage.url] });
console.log("B 站發佈成功");
}
broadcastWeeklyReport("本週技術週報...", "./weekly.png");
開發者與 AI 的對話記錄
Ponytail (極簡主義者): 「這個腳本很直觀啊,從上往下依序發佈。我們只需要設置一個 Cron Job (排程任務) 每個禮拜五跑一次就行了。」
Agent Reach (容錯專家): 「這種設計犯了分散式系統中的『級聯失敗 (Cascading Failure)』錯誤。如果 Twitter 的 API 暫時當機或 Token 失效,B 站的發佈也會被迫中斷。各平台的發佈邏輯應該是解耦 (Decoupled) 且相互獨立的。」
Ponytail: 「好麻煩,那用 Promise.allSettled() 把它們平行化處理可以嗎?」
Agent Reach: 「這是個好主意,但我們還需要考慮更深層的防護。例如:B 站上傳圖片時可能會因為檔案過大而報錯;Twitter 的 OAuth2.0 Token 可能需要透過 Refresh Token 動態更新。我們必須為每個平台設計獨立的隔離艙 (Bulkhead Pattern),並加入 Token 的輪替 (Rotation) 機制。」
重構/優化後的代碼:100% 保留安全防護與邊界檢查
重構後的系統採用非同步獨立任務與策略模式 (Strategy Pattern),將不同平台的發佈邏輯隔離,並加入了健全的錯誤捕獲與重試。
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
// 模擬各平台的 SDK
const twitterApi = require('./mock/twitter-api');
const bilibiliApi = require('./mock/bilibili-api');
const tokenManager = require('./utils/token-manager');
// 定義統一日誌介面
const logger = {
info: (msg) => console.log(`[INFO] ${new Date().toISOString()} - ${msg}`),
error: (msg, err) => console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err)
};
/**
* 平台發佈策略的基礎類別
*/
class PublishStrategy {
async publish(content, imagePath) {
throw new Error("必須實作 publish 方法");
}
}
class TwitterStrategy extends PublishStrategy {
async publish(content, imagePath) {
let retries = 3;
while (retries > 0) {
try {
// 1. 確保 Token 有效性
const token = await tokenManager.getValidToken('twitter');
// 2. 邊界檢查:圖片是否存在與大小驗證
const stats = fs.statSync(imagePath);
if (stats.size > 5 * 1024 * 1024) {
throw new Error("Twitter 圖片大小不能超過 5MB");
}
const imageBuffer = fs.readFileSync(imagePath);
const media = await twitterApi.uploadMedia(imageBuffer, token);
await twitterApi.postTweet(content, media.id, token);
logger.info("Twitter 發佈成功");
return { platform: 'Twitter', status: 'success' };
} catch (error) {
if (error.code === 'TOKEN_EXPIRED') {
logger.info("Twitter Token 過期,嘗試刷新...");
await tokenManager.refreshToken('twitter');
} else {
retries--;
logger.error(`Twitter 發佈失敗,剩餘重試次數: ${retries}`, error.message);
if (retries === 0) return { platform: 'Twitter', status: 'failed', error: error.message };
await new Promise(res => setTimeout(res, 2000)); // 簡單延遲重試
}
}
}
}
}
class BilibiliStrategy extends PublishStrategy {
async publish(content, imagePath) {
try {
const token = await tokenManager.getValidToken('bilibili');
const imageBuffer = fs.readFileSync(imagePath);
// 處理 B 站專有的圖片上傳邏輯與逾時控制 (Timeout Control)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超時
const imageUrl = await bilibiliApi.uploadImage(imageBuffer, token, { signal: controller.signal });
clearTimeout(timeoutId);
await bilibiliApi.postDynamic(content, [imageUrl], token);
logger.info("B 站發佈成功");
return { platform: 'Bilibili', status: 'success' };
} catch (error) {
logger.error("B 站發佈遇到不可恢復錯誤", error.message);
return { platform: 'Bilibili', status: 'failed', error: error.message };
}
}
}
/**
* 廣播管理器
*/
async function broadcastToAllPlatforms(content, imagePath) {
if (!fs.existsSync(imagePath)) {
logger.error(`圖片路徑無效: ${imagePath}`);
return;
}
const strategies = [
new TwitterStrategy(),
new BilibiliStrategy()
];
// 使用 Promise.allSettled 進行隔離執行,避免單點故障影響全域
const results = await Promise.allSettled(
strategies.map(strategy => strategy.publish(content, imagePath))
);
// 產出執行報表
logger.info("=== 發佈任務總結 ===");
results.forEach((res, index) => {
if (res.status === 'fulfilled') {
const data = res.value;
logger.info(`- ${data.platform}: ${data.status === 'success' ? '✅ 成功' : '❌ 失敗 (' + data.error + ')'}`);
} else {
logger.error(`- 平台任務崩潰`, res.reason);
}
});
}
// 執行排程任務
broadcastToAllPlatforms("【Agent Reach 開源週報】本週我們更新了容錯模組...", "./assets/weekly_cover.png");
效益分析表格與解讀
| 設計考量點 | 原始設計 | 重構後設計 (Agent Reach) | 帶來的優勢 |
|---|---|---|---|
| 任務耦合度 | 序列執行 (串列耦合) | 平行執行 (Promise.allSettled) |
實現了隔離艙模式,平台間互不干擾 |
| 憑證管理 | 靜態寫死 Token | 結合 TokenManager 動態刷新 |
解決了 OAuth Token 短期失效的問題,確保長期無人值守運行 |
| 大文件處理邊界 | 讀取即上傳,無視大小 | 加入 fs.statSync 前置校驗 |
避免消耗無效帶寬,提前攔截平台端報錯 |
| 超時控制 | 無 | 導入 AbortController 實現逾時終止 |
避免網路掛起 (Hang) 導致排程任務殭屍化 |
解讀:
本範例展示了在構建排程廣播系統時,如何透過「防禦性編程」理念保護主程序的執行。使用 Promise.allSettled 代替簡單的 await 鏈,保證了多目標發佈的獨立性。同時,針對網路請求加入了基於 AbortController 的超時機制與基於檔案大小的邊界校驗,使得這套腳本具備了上線運作 (Production-ready) 的可靠水準。