1---2name: canonical-schema3description: canonical schema 字段语义与各 provider 原始格式要点(gmail/outlook/google-calendar)。产出格式映射时使用。4---56# canonical schema 字段语义78## 邮件(recordKind = "mail")910- `providerMessageId`(必填):provider 内全局唯一消息 id(Gmail `id`;Outlook `id`)。11- `providerThreadId`:会话 id(Gmail `threadId`;Outlook `conversationId`)。12- `subject` / `snippet`:主题;正文摘要(Gmail `snippet`;Outlook `bodyPreview`)。13- `textBody` / `htmlBody`:正文明文与 HTML;两者都有时都输出。14- `receivedAt` / `sentAt`:ISO-8601。15- `isRead` / `isStarred` / `isDraft`:布尔。16- `addresses`:`{role, address, displayName?}` 数组;role 枚举 from/sender/to/cc/bcc/reply-to。17- `memberships`:Gmail labelIds 原样(不含语义推导)。18- `attachments`:`{providerId?, filename?, mimeType?, size?, inline?}`。1920## 日程(recordKind = "calendar")2122- `providerEventId`(必填)、`title`(必填,缺失时输出 `"(无标题)"`)、23 `startsAt` / `endsAt`(必填,ISO-8601)。24- `allDay`:全天事件(Google `start.date` 无 dateTime 时为 true)。25- `timeZone` / `location` / `description` / `status` / `providerRevision`(etag)。26- `organizer` / `attendees`:`{role, address, displayName?}`;日程专用 role:27 organizer 用 `"organizer"`、参与人用 `"attendee"`。28- 墓碑:Google `status === "cancelled"` → isTombstone。2930# provider 原始格式要点3132## gmail(映射输入 = Gmail REST 资源形状)3334- `payload.headers`:`{name, value}` 数组,name 大小写不敏感 → 地址字段。35- 正文:`payload` MIME 树递归,`mimeType="text/plain"` / `"text/html"` 的36 `body.data` 是 base64url;附件节点有 `body.attachmentId`。37- `labelIds` 数组:`UNREAD`(无 → isRead=true)、`STARRED`、`DRAFT`。38- `internalDate`:毫秒字符串 → `$fromMillis($number(internalDate))`;`historyId` → providerRevision。3940## outlook(映射输入 = Microsoft Graph 消息对象)4142- `@removed` 存在 → 墓碑(tombstoneId = `$string(id)`)。43- 地址:`from`/`sender`(单对象)、`toRecipients`/`ccRecipients`/`bccRecipients`/`replyTo`44 (数组),均为 `{emailAddress: {name, address}}` → role 依次映射 sender/to/cc/bcc/reply-to。45- `flag.flagStatus === "flagged"` → isStarred;`isRead`/`isDraft` 直取。46- `body.contentType` "text"|"html" → textBody|htmlBody(content);47 `receivedDateTime`/`sentDateTime` 已是 ISO;`changeKey` → providerRevision。4849## google-calendar(映射输入 = Google Calendar API v3 event)5051- `start`/`end`:`dateTime`(含时区)或 `date`(全天 → 追加 `T00:00:00Z`,allDay=true)。52- `status === "cancelled"` → 墓碑;`etag` → providerRevision;`recurrence` → recurrence.rules。53- `organizer`/`attendees`:`{email, displayName, ...}`。5455# JSONata 表达式注意事项(实测)5657- **单匹配退化**:过滤或对象构造(`x[cond].{...}`)在只有 1 个匹配时结果是58 单对象而非数组。canonical 的数组字段(addresses / attendees / memberships /59 attachments)必须用数组构造包裹保证类型:`[x[cond].{'role': ..., 'address': ...}]`。60 包裹后无匹配产出 `[]`(合法)。61- **特殊字符字段名用反引号**:`` $exists(`@removed`) ``、`` `@odata.deltaLink` ``。62- **数组直接当条件会逐项映射**:存在性/判空一律 `$count(x) > 0` 或63 `$count($filter(labelIds, function($v){$v='UNREAD'})) = 0`。64- **`$append` 只接受 2 个参数**:多段拼接需嵌套并数组包裹:65 `[$append($append(a, b), c)]`。66- **`$flatten` 不可用**(T1006 "Attempted to invoke a non-function")。展平一层67 数组的数组用 `$reduce($blocks, $append, [])`(`$append` 会拼接第二参数的元素)。68- **对象构造器不对函数调用结果扇出**:`{'address': $split(value, ',')}` 得到的69 `address` 是数组而非多条目。逐条目产出必须显式 `$map(..., function($p){ {...} })`。70- **多地址头**(`To: a@x.com, "Li, Si" <b@y.com>`):禁止按逗号 `$split`(引号显示名71 含逗号会切碎),也禁止 `$match(...)` 不取 `.match`(address 会变成数组)。用72 `$match($h.value, /[\w.+-]+@[\w.-]+/).match` 逐匹配提取,配 `$map` 构造条目、73 `$reduce($blocks, $append, [])` 展平。74- **总表达式纪律**:每个 record 表达式对所有真实记录都不能抛错——可选路径先75 `$exists(...)` 守卫(`$exists(x) ? f(x) : undefined`),否则一条形状不同的76 真实邮件就会让整轮 run 以 format_mapping_pending 失败。77- 递归下降收集 MIME 节点用 `**[mimeType='text/plain'][0].body.data`78 (`($**)` 语法不合法)。79- 无匹配的字段表达式返回 undefined,服务端自动略过该字段;不要用占位值80 (title 必填除外,canonical 规定缺失时输出 `"(无标题)"`)。