跳转至

Targetless Presentation(无设备呈现)

Targetless Presentation 允许已认证用户在不拥有或注册任何 inklet 显示屏的情况下生成可视化内容。生成结果包含一份 inklet Scene v1 JSON 文档和零至多张 PNG 渲染图(rendition)。

关键约束: 生成过程不会创建任何 Device 行、发布 Push、发送 MQTT,也不依赖任何兼容的显示屏。


认证

Targetless Presentation 挂载在两个前缀下,底层逻辑完全一致:

前缀 认证方式 使用方
/api/sdk/v1 PAT(Bearer il_pat_... SDK、服务端集成
/api/app/v1 用户访问令牌(JWT) macOS / iOS / Portal 客户端

/api/app/v1 支持 X-Renewed-Token 滑动续期;/api/sdk/v1 不下发该 header。


权限

Mode Free Pro
mode=auto(AI 生成) 返回 403 plan_upgrade_required 允许
mode=hardcode(图片直传) 允许 允许

降级后仍可读取已有 Presentation、list、续签 URL,以及对已存 Scene 追加新尺寸 rendition——这些操作不重新触发 AI,也不受 Pro gate 拦截。


创建 Targetless Content

POST /contents 请求中加入 output 字段即可创建 targetless Content。只要 output 存在,处理路径就会走 targetless 分支而非 Display 路由。

POST /api/sdk/v1/contents
Idempotency-Key: <8-128 printable ASCII>
Content-Type: application/json
Authorization: Bearer il_pat_...
{
  "mode": "auto",
  "intent": "创建一份简洁的每周摘要",
  "title": "本周摘要",
  "output": {
    "preset": "macos-widget-medium",
    "formats": ["scene", "png"],
    "colorMode": "color"
  },
  "assets": [
    { "type": "text", "text": "本周营收增长 12%。" }
  ]
}

output 字段说明

字段 类型 说明
preset string? 预设名称;与 viewport 互斥
viewport object? { "width": N, "height": N };与 preset 互斥
formats string[] ["scene", "png"];默认 ["scene", "png"]
colorMode string color / grayscale / monochrome;默认 color

presetviewport 若均未指定,使用默认 800×480 预览 profile。Viewport 的 width/height 为整数,范围 1...8192,且单张渲染总像素不得超过 4,000,000

预设注册表

Preset Viewport 默认 colorMode
default 800×480 color
macos-widget-small 170×170 color
macos-widget-medium 360×170 color
macos-widget-large 360×376 color

各 mode 规则

Mode displayId output 含义
auto 禁止(null 或省略) 必须有 AI 分析,targetless 输出
hardcode 禁止(null 或省略) 必须有 图片直传,无 AI
manual 必填 不得有 Display 专属,不可 targetless

manual + outputoutput + displayId(非 null)均返回 400 invalid_request,且由 DB CHECK 约束在写入层拦截。

归一化的 Content output 字段

{
  "output": {
    "formats": ["scene", "png"],
    "preset": "macos-widget-medium",
    "viewport": { "width": 360, "height": 170 },
    "colorMode": "color"
  }
}

旧的面向 Display 的 Content,outputnull


处理流程

Auto targetless

confirm → job → 验证 assets → 抓取链接 → AI 分析 →
  生成 Scene v1 → 持久化 Presentation + Content 链接 →
    Content 标记 ready → 排入 PNG rendition 队列

全程不进入 Display 路由、不创建 Push 行、不发送 MQTT。Scene 生成可能先于 PNG 渲染完成;此时 Presentation 处于 preparing,但 Content 已是 ready(Presentation ID 已持久化)。

Hardcode targetless

confirm → job → 生成 Scene(viewport fill, fit: "stretch") →
  持久化 Presentation → 排入 PNG rendition 队列

只接受恰好一张 PNG/JPEG;不执行 AI 分析、摘要、模板选择。


Scene v1

Media type: application/vnd.inklet.scene+json;version=1

{
  "scene": {
    "mediaType": "application/vnd.inklet.scene+json;version=1",
    "version": 1,
    "data": {
      "version": 1,
      "viewport": { "width": 360, "height": 170 },
      "background": "#ffffff",
      "elements": [
        {
          "id": "headline",
          "type": "text",
          "frame": { "x": 20, "y": 20, "width": 320, "height": 80 },
          "properties": {
            "text": "营收增长 12%",
            "fontSize": 28,
            "fontWeight": 600,
            "color": "#000000",
            "align": "leading"
          }
        }
      ]
    }
  }
}

元素类型

text

属性 类型 默认值 说明
text string 必填,显示文本
fontSize number 16 字号(pt)
fontWeight number 400 < 600 为 Regular,≥ 600 为 Bold
color string #000000 十六进制颜色
align string leading leading / center / trailing

超框文字加省略号截断。含 CJK 码点自动选用 Noto CJK 字体,其余使用 DejaVu。

image

属性 类型 默认值 说明
fileId string 必填,自有资源的文件 ID
fit string contain contain / cover / fill / stretch

API 响应中 fileId 会被替换为 15 分钟有效期的签名读取 URL(url 字段),但 持久化的 Scene 只存 fileId,不存凭据或永久签名 URL。

shape

属性 类型 默认值 说明
shapeType string rect rect / ellipse
fill string #000000 十六进制填充色
cornerRadius number 0 圆角半径(rect 专用)
strokeColor string? null 描边色
strokeWidth number 0 描边宽度

通用规则

  • Frame 坐标为整数,在 Scene viewport 坐标系内。
  • widthheight 必须为正数。
  • 同一 Scene 内元素 id 唯一。
  • 元素层叠顺序 = 文档顺序(后写的盖前面的,无 z-index)。
  • Frame 越界部分裁剪;alpha 合成到白底。
  • 非法值(枚举外、类型错误)降级到默认值;非法结构(缺少必填字段)返回 400。
  • 未知可选属性由存储层保留,renderer 忽略;未知元素类型跳过。
  • Scene 中严禁出现 PAT、用户访问令牌、预签名上传 policy、私有 object key 或第三方永久凭据。

Presentation 响应模型

{
  "id": "019...",
  "displayId": null,
  "contentIds": ["019..."],
  "mode": "auto",
  "state": "ready",
  "output": {
    "formats": ["scene", "png"],
    "preset": "macos-widget-medium",
    "viewport": { "width": 360, "height": 170 },
    "colorMode": "color"
  },
  "scene": {
    "mediaType": "application/vnd.inklet.scene+json;version=1",
    "version": 1,
    "data": { "version": 1, "viewport": { "width": 360, "height": 170 }, "background": "#ffffff", "elements": [] }
  },
  "renditions": [
    {
      "id": "019...",
      "mediaType": "image/png",
      "format": "png",
      "width": 360,
      "height": 170,
      "colorMode": "color",
      "state": "ready",
      "url": "https://...",
      "expiresAt": "2026-09-01T20:15:00Z",
      "failure": null,
      "updatedAt": "2026-09-01T20:00:00Z"
    }
  ],
  "image": null,
  "failure": null,
  "createdAt": "2026-09-01T20:00:00Z",
  "updatedAt": "2026-09-01T20:01:00Z"
}

Targetless vs Display Presentation 的差异

字段 Targetless Display
displayId null UUID 字符串
scene Scene v1 对象 null
renditions rendition 列表 []
image null 渲染图对象
state preparing / ready / failed preparing / queued / published / confirmed / expired / failed

Presentation 聚合状态

只有初始 rendition(来自 Content output.formats)决定 Presentation 状态:

Presentation state 含义
preparing Scene 已落库,初始 rendition 尚未完成
ready 初始 rendition 全部 readyfailed
failed Scene 本身生成失败(终态)

后续追加的 rendition 无论成功失败均不改变 Presentation 的聚合状态。

Rendition 状态

state url / expiresAt failure
preparing null null
ready 签名 URL + 过期时间 null
failed null 结构化 problem
  • url 签名有效期约 15 分钟;GET Presentation 重签同一文件,不重新渲染。
  • failed 状态下 rendition 仍可查询(不是 404)。
  • 单个 rendition 的失败不影响 Presentation 或其他已有 rendition。

API 接口

GET /presentations

GET /api/sdk/v1/presentations?scope=generated&state=ready&limit=20
GET /api/app/v1/presentations?scope=generated&state=ready&limit=20
参数 可选值 默认
scope generated(targetless)/ display / all generated
state preparing / ready / failed 全部
limit 1–50 20

返回标准 cursor 分页信封 { items, nextCursor, hasMore }

GET /presentations/{id}

GET /api/sdk/v1/presentations/{presentationId}
GET /api/app/v1/presentations/{presentationId}

纯读操作:可续签 rendition URL 和 Scene 内 image asset URL,但不渲染、不发布、不唤醒 Display。

  • 不属于你的 Presentation → 404 presentation_not_found

POST /presentations/{id}/renditions

为已有 Presentation 追加新尺寸 PNG。重用已持久化的 Scene,不重跑 AI

POST /api/sdk/v1/presentations/{presentationId}/renditions
POST /api/app/v1/presentations/{presentationId}/renditions
Idempotency-Key: optional-but-recommended
{
  "formats": ["png"],
  "viewport": { "width": 720, "height": 340 },
  "colorMode": "color"
}

去重键: (presentation_id, format, width, height, color_mode)。并发的相同请求返回同一个 rendition,不创建重复渲染任务。

响应 202(异步):

{
  "id": "019...",
  "format": "png",
  "width": 720,
  "height": 340,
  "colorMode": "color",
  "state": "preparing",
  "url": null,
  "expiresAt": null,
  "failure": null,
  "updatedAt": "2026-09-01T20:05:00Z"
}

之后通过 GET /presentations/{id} 轮询完成态。


PNG 渲染缩放规则

情况 规则 结果
请求尺寸与 Scene viewport 同比例 整体等比放大 精确等比放大,零留白(含 Retina/高清输出)
请求尺寸与 Scene viewport 不同比例 min(sx, sy) 居中缩放 Scene 背景色填充留白,不变形不裁剪
Hardcode 直接各向异性拉伸 铺满输出尺寸,不保持宽高比,不裁剪,不留白

同比例判断使用整数叉乘而非浮点比较,以避免 Retina 场景下的一像素黑边。


资源限制

限制 v0.1 默认值 环境变量 超限错误码
单张渲染总像素 4,000,000 TARGETLESS_MAX_RENDITION_PIXELS 400 invalid_request
单 Presentation 累计 rendition 数 20 TARGETLESS_MAX_RENDITIONS_PER_PRESENTATION 409 invalid_state
单用户并发 preparing rendition 数 8 TARGETLESS_MAX_CONCURRENT_RENDITIONS 429 rate_limited + Retry-After
  • 像素限制同时在 Content 创建和 POST /renditions 两处生效。
  • 并发限制仅针对真实新建的 rendition;命中去重的请求不计入并发(否则破坏幂等性)。
  • 配置值 0 或未设置时回落到上述安全默认值,不等同于无限制。

macOS 应用接入流程

macOS 客户端使用 /api/app/v1 和现有访问令牌:

  1. POST /api/app/v1/contents,带 output.preset=macos-widget-medium(或其他 preset)
  2. 将二进制 asset 上传到返回的预签名票据(不带 Authorization 头)
  3. POST /api/app/v1/contents/{id}/confirm
  4. 轮询 GET /api/app/v1/contents/{id}state=readyfailed
  5. presentationIds[0] 读取 Presentation
  6. 下载最匹配当前 Widget 族尺寸的 PNG rendition
  7. 将 PNG 和 Scene JSON 写入 Widget App Group 共享目录
  8. 重载 Widget timeline

Widget 本身不持有 PAT 或访问令牌,不依赖签名 URL 保持有效。Host app 负责网络访问,为 WidgetKit 写入持久本地缓存文件。


错误码

状态码 Code 触发场景
400 invalid_request output 字段非法、preset/viewport 同时指定等
400 invalid_request 超出像素限制(在创建端或 rendition 端)
402 payment_required 订阅扣款失败
403 plan_upgrade_required Free 用户请求 mode=auto targetless
404 presentation_not_found Presentation 不存在或不属于你
409 invalid_state 该 Presentation 的 rendition 数量已达上限(永久)
429 rate_limited 超出并发 rendition 上限;附 Retry-After 响应头

权限拒绝码说明

本实现沿用现有 entitlement 系统的 plan_upgrade_requiredpayment_required,而非原始契约中的 subscription_required。两者语义相同,但这是本仓库公开 API 和 portal 的既有约定,不做更改以保持兼容性。


TypeScript 类型定义

// ----- Output Profile -----
type ColorMode = "color" | "grayscale" | "monochrome";
type OutputFormat = "scene" | "png";

interface OutputProfile {
  formats: OutputFormat[];
  preset: string | null;
  viewport: { width: number; height: number };
  colorMode: ColorMode;
}

// ----- Scene v1 -----
type ElementType = "text" | "image" | "shape";

interface Frame { x: number; y: number; width: number; height: number; }

interface SceneElement {
  id: string;
  type: ElementType;
  frame: Frame;
  properties: Record<string, unknown>;
}

interface SceneData {
  version: 1;
  viewport: { width: number; height: number };
  background: string;
  elements: SceneElement[];
}

interface SceneV1 {
  mediaType: "application/vnd.inklet.scene+json;version=1";
  version: 1;
  data: SceneData;
}

// ----- Rendition -----
type RenditionState = "preparing" | "ready" | "failed";

interface Rendition {
  id: string;
  mediaType: "image/png";
  format: "png";
  width: number;
  height: number;
  colorMode: ColorMode;
  state: RenditionState;
  url: string | null;
  expiresAt: string | null;
  failure: Problem | null;
  updatedAt: string;
}

// ----- Targetless Presentation -----
type TargetlessPresState = "preparing" | "ready" | "failed";

interface TargetlessPresentation {
  id: string;
  displayId: null;
  contentIds: string[];
  mode: "auto" | "hardcode";
  state: TargetlessPresState;
  output: OutputProfile;
  scene: SceneV1 | null;
  renditions: Rendition[];
  image: null;
  failure: Problem | null;
  createdAt: string;
  updatedAt: string;
}

v0.2 中明确推迟的功能

  • 自定义 Display 注册
  • 设备 enrollment token 与 BYOD
  • capability 上报
  • Quote/0 provider 凭据与适配器
  • 通用 Display pull 协议
  • Scene 投递 / 确认语义
  • 动态设备传输协议