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/Shanghai" |
context.locale | "zh-Hans" |
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 响应——一切都在真正发送之前。
- 保存。此后发往该订户的每次发行都过你的脚本;脚本出错会在发行台账里 明白地失败,而不是把垃圾送出门。
脚本会进配置备份
与密钥不同(密钥在钥匙串,永不导出),转换脚本会原文进入配置备份—— 别把凭证硬编码在脚本里。