JavaScript 轉換腳本
預設情況下,訂戶收到的是原樣的 canonical JSON 紀錄。當你的伺服器想要別的 ——Markdown、另一種 JSON 結構、一行純文字——就給這個訂戶掛一個轉換 腳本,自行產生請求主體。
契約
你只需提供一個函式:
js
function transform(data, context) {
// 回傳 HTTP 請求主體
}- 回傳字串 → 原樣傳送(Markdown、純文字、表單編碼……)。
- 回傳物件或陣列 → 傳送
JSON.stringify(...)的結果,Content-Type保持application/json。 - 回傳其他任何東西(
undefined、數字、布林值)→ 本次傳送失敗, 這是有意設計——Courier 絕不傳送空的或半成品資料。
腳本只控制請求主體。URL、方法、請求標頭與驗證維持訂戶上的設定不變。掛了 腳本後,靜態 Body 參數會被繞過——請求主體完全由你產生。
沙盒
腳本在本機一個封閉的 JavaScriptCore 沙盒裡執行:
- 無網路、無檔案系統、無 DOM、無
require、無setTimeout。 - 報錯與無窮迴圈都會判本次傳送失敗,原因顯示在發行台帳裡。
console.log/warn/error會被捕捉,僅在測試預覽裡顯示。
context 參考
所有值均為字串:
| 欄位 | 範例 |
|---|---|
context.endpointName | "我的 Obsidian 中繼" |
context.url | 目的地 URL |
context.kind | "workout" | "sleep" | "note" | "weather" |
context.now | 本次發行的 ISO-8601 時間戳記 |
context.timeZone | "Asia/Taipei" |
context.locale | "zh-Hant" |
context.appVersion / context.appBuild | "1.0.0" / "1" |
context.idempotencyKey | 本次傳送將攜帶的 Idempotency-Key |
data 參考
data 是該訂戶類型的 canonical 紀錄。日期一律為 ISO-8601 字串(用 new Date(data.start) 解析);可選欄位可能不存在。
kind === "workout"(運動)
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | HealthKit UUID——冪等之根 |
activityType | string | "running"、"cycling"、"walking"…… |
source | object | { name, bundleIdentifier?, productType?, version? } |
start、end | string | ISO-8601 |
duration | number | 秒 |
distanceMeters | number? | 公尺 |
activeEnergyKcal、totalEnergyKcal | number? | 大卡 |
elevationGainMeters | number? | 公尺 |
heartRate | object? | { averageBpm?, minBpm?, maxBpm? } |
avgPaceSecPerKm | number? | 秒/公里 |
laps | array? | [{ index, start, duration, distanceMeters? }] |
heartRateSeries | array? | 僅當「含心率序列」開啟 |
route | array? | 僅當「含路線」開啟;體積大 |
metadata | object? | HealthKit metadata 原樣傳遞 |
schemaVersion | number |
kind === "sleep"(睡眠)
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | |
nightOf | string | 這一晚歸屬的日曆日 |
inBedStart、inBedEnd | string? | |
sleepStart、sleepEnd | string | |
timeInBedSec | number? | 秒 |
totalAsleepSec | number | 秒 |
stages | object | { awakeSec, remSec, coreSec, deepSec, unspecifiedSec } |
awakenings | number | 醒來次數 |
efficiencyPct | number? | 0–100 |
vitals | object? | { averageHeartRateBpm?, averageHRVms?, respiratoryRate?, oxygenSaturation?, wristTemperatureDeltaC? } |
segments | array | [{ stage, start, end }],stage 取值 inBed|awake|rem|core|deep|unspecified |
sources | array | [{ name, bundleIdentifier?, … }] |
schemaVersion | number |
kind === "note"(批註)
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | |
createdAt | string | ISO-8601 |
text | string | 記下的想法(打字或轉寫) |
source | string | "text" | "audio" |
metadata | object? | |
schemaVersion | number |
kind === "weather"(天氣)
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | "{sleepID}:weather" |
capturedAt | string | 測量時刻(≈ 醒來時) |
weatherDate | string | 伴隨睡眠的 nightOf |
symbolName | string | SF Symbol 風格,如 "cloud.sun" |
conditionText | string | 如 "Cloudy" |
temperatureC、apparentTemperatureC | number | 攝氏 |
humidityPct | number | 0–100 |
windKmh | number | |
uvIndex | number | |
precipitationChancePct | number | 0–100 |
location | object? | { lat, lon }——僅當「含定位座標」開啟 |
schemaVersion | number |
範例
自訂 JSON 結構:
js
function transform(data, context) {
return {
kind: "workout",
activity: data.activityType,
km: data.distanceMeters ? +(data.distanceMeters / 1000).toFixed(2) : null,
minutes: Math.round(data.duration / 60),
avgHr: (data.heartRate && data.heartRate.averageBpm) || null,
at: data.start,
via: context.endpointName,
};
}一行純文字:
js
function transform(data) {
const h = Math.floor(data.totalAsleepSec / 3600);
const m = Math.round((data.totalAsleepSec % 3600) / 60);
return "睡了 " + h + " 小時 " + m + " 分(" + data.nightOf.slice(0, 10) +
",醒來 " + data.awakenings + " 次)";
}批註變 Markdown 引用區塊:
js
function transform(data) {
return "> " + data.text + "\n>\n> — " + data.createdAt;
}工作流程
- 在訂戶編輯頁展開腳本區,匯入
.js檔案或貼上程式碼。App 可直接給你 一份註解完整的範本當起點。 - 點測試:腳本對一條近期樣本執行,你會看到確切的輸出主體、console 日誌 與端點的 HTTP 回應——一切都在真正傳送之前。
- 儲存。此後發往該訂戶的每次發行都會經過你的腳本;腳本出錯會在發行台帳裡 明白地失敗,而不是把垃圾送出門。
腳本會進設定備份
與金鑰不同(金鑰在鑰匙圈,永不匯出),轉換腳本會原文進入設定備份—— 別把憑證寫死在腳本裡。