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 |
preset 与 viewport 若均未指定,使用默认 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 + output、output + 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,output 为 null。
处理流程¶
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¶
只接受恰好一张 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 坐标系内。
width与height必须为正数。- 同一 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 全部 ready 或 failed |
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}¶
纯读操作:可续签 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
去重键: (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 和现有访问令牌:
POST /api/app/v1/contents,带output.preset=macos-widget-medium(或其他 preset)- 将二进制 asset 上传到返回的预签名票据(不带 Authorization 头)
POST /api/app/v1/contents/{id}/confirm- 轮询
GET /api/app/v1/contents/{id}至state=ready或failed - 从
presentationIds[0]读取 Presentation - 下载最匹配当前 Widget 族尺寸的 PNG rendition
- 将 PNG 和 Scene JSON 写入 Widget App Group 共享目录
- 重载 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_required 和 payment_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 投递 / 确认语义
- 动态设备传输协议