Skip to content

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"(运动)

字段类型说明
idstringHealthKit UUID——幂等之根
activityTypestring"running""cycling""walking"……
sourceobject{ name, bundleIdentifier?, productType?, version? }
startendstringISO-8601
durationnumber
distanceMetersnumber?
activeEnergyKcaltotalEnergyKcalnumber?千卡
elevationGainMetersnumber?
heartRateobject?{ averageBpm?, minBpm?, maxBpm? }
avgPaceSecPerKmnumber?秒/公里
lapsarray?[{ index, start, duration, distanceMeters? }]
heartRateSeriesarray?仅当「含心率序列」开启
routearray?仅当「含路线」开启;体积大
metadataobject?HealthKit metadata 透传
schemaVersionnumber

kind === "sleep"(睡眠)

字段类型说明
idstring
nightOfstring这一晚归属的日历日
inBedStartinBedEndstring?
sleepStartsleepEndstring
timeInBedSecnumber?
totalAsleepSecnumber
stagesobject{ awakeSec, remSec, coreSec, deepSec, unspecifiedSec }
awakeningsnumber觉醒次数
efficiencyPctnumber?0–100
vitalsobject?{ averageHeartRateBpm?, averageHRVms?, respiratoryRate?, oxygenSaturation?, wristTemperatureDeltaC? }
segmentsarray[{ stage, start, end }],stage 取值 inBed|awake|rem|core|deep|unspecified
sourcesarray[{ name, bundleIdentifier?, … }]
schemaVersionnumber

kind === "note"(批注)

字段类型说明
idstring
createdAtstringISO-8601
textstring记下的想法(打字或转写)
sourcestring"text" | "audio"
metadataobject?
schemaVersionnumber

kind === "weather"(天气)

字段类型说明
idstring"{sleepID}:weather"
capturedAtstring测量时刻(≈ 醒来时)
weatherDatestring伴随睡眠的 nightOf
symbolNamestringSF Symbol 风格,如 "cloud.sun"
conditionTextstring"Cloudy"
temperatureCapparentTemperatureCnumber摄氏
humidityPctnumber0–100
windKmhnumber
uvIndexnumber
precipitationChancePctnumber0–100
locationobject?{ lat, lon }——仅当「含定位坐标」开启
schemaVersionnumber

示例

自定义 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;
}

工作流

  1. 在订户编辑页展开脚本区,导入 .js 文件或粘贴代码。App 可直接给你 一份注释完整的模板当起点。
  2. 测试:脚本对一条近期样本运行,你会看到确切的输出体、console 日志 与端点的 HTTP 响应——一切都在真正发送之前。
  3. 保存。此后发往该订户的每次发行都过你的脚本;脚本出错会在发行台账里 明白地失败,而不是把垃圾送出门。

脚本会进配置备份

与密钥不同(密钥在钥匙串,永不导出),转换脚本会原文进入配置备份—— 别把凭证硬编码在脚本里。

印于自家 · © 2026 Xheldon