Joye Dev

Back

feat(export): add PNG export size option, default 1x with paid 2x tier

这条业务线解决的是“用户从编辑器导出可交付图片”时的默认值、清晰度、批量导出和跨页面类型一致性问题。此前 PNG 的 2× 来自 export worker 契约的隐式默认,用户不能选择,产物体积偏大;DESIGN、CODE、多页 ZIP、MP4 和 agent snapshot 也没有统一表达导出尺度的入口


正文来源:飞书学习文档。以下为通过个人 Feishu API 获取并转换后的完整 Markdown 正文。

业务线概览#

这条业务线解决的是“用户从编辑器导出可交付图片”时的默认值、清晰度、批量导出和跨页面类型一致性问题。此前 PNG 的 2× 来自 export worker 契约的隐式默认,用户不能选择,产物体积偏大;DESIGN、CODE、多页 ZIP、MP4 和 agent snapshot 也没有统一表达导出尺度的入口。

本次范围覆盖 site 的导出面板与请求、Next.js 导出路由、共享导出编排、export worker 合约,以及 agent snapshot 工具。它没有完成服务端付费 entitlement 强制;2× 仍主要是 UI 层的产品门禁。

**业务线名称:**统一 PNG 导出尺度与跨编辑器渲染边界。
**当前范围:**DESIGN 单页、多页 PNG ZIP、CODE 单页 PNG、MP4 默认尺度、selection 导出和 agent snapshot 默认尺度。
**本次新增:**把 scale 变成调用方显式传递的参数,PNG 主流程默认 1×,付费用户可选择 2×,并把旧 worker fallback 留在兼容边界。

今日锚点#

**PR #4938:**feat(export): add PNG export size option, default 1x with paid 2x tier

  • 作者:luin
  • 合入时间:2026-07-20T04:01:56Z,即 Australia/Melbourne 2026-07-20 14:01:56 AEST
  • 链接:GitHub PR #4938
  • 合入提交:36f08ff8be6a33717fa6f6860f8d8c3db9f30aa1,已验证可达 origin/main

它是今天最合适的入口,因为它不是孤立的 UI 选项:12 个文件把 scale 从 ExportPanel 传到单页/ZIP/CODE/MP4/agent snapshot 的边界,并修正了多个隐藏默认。它还能用 #4706、#4626、#4837 的真实代码自然串起 CODE 出口、多页边界和用户入口演进;今天的其他候选中,依赖升级和纯 UI 抽取的工程密度较低,agent 小修也与最近两次 agent 可靠性学习重复度更高。

演进时间线#

阶段PR改变的层代码证据与意义
前置:跨编辑器单页出口#4706,作者 yansanmu1990,2026-07-15 05:57:08 AESTIDL / Go API / site 编排 / export workerCODE 不再复用 DESIGNValue 渲染,而是发送原始 HTML;Go API 为 workspace 签发 10 分钟 worker token,Playwright 在 worker 内解析 assets.local 和字体,并限制外部网络、捕获尺寸和执行时限。
前置:批量 PNG 能力#4626,作者 lawvs,2026-07-15 18:43:42 AESTsite route / export contract / worker handler / 测试增加有序多页 ZIP;最多 20 页,累计 PNG 输入超过 32 MiB 返回 413;worker 串行截图、按页裁剪资源集合、生成稳定的 page-NNN.png,并用测试锁住顺序、资源隔离和内存边界。
前置:用户入口收敛#4837,作者 litemo,2026-07-16 00:08:58 AESTsite ExportPanel / 行为测试把 DESIGN 导出的初始格式从 PDF 改成当前页 PNG,并用 ExportButton 测试固定默认行为,为今天把默认 1× 放在 PNG 主流程提供了稳定入口。
当前锚点:尺度产品化#4938,作者 luin,2026-07-20 14:01:56 AESTsite UI / billing UX / route contract / worker caller / agent toolPNG 面板新增 Standard 1× 与 High resolution 2×;free 用户点击 2× 进入 pricing dialog;billing loading/error 时对付费项 fail closed;所有主调用方显式传 scale,路由默认明确区分 1× 与 selection 的 2×。

以上四个 PR 的 merge commit 均已验证可达 origin/main;没有把未合入分支或仅存在于 PR 分支的改动纳入报告。

当前架构与数据流#

用户入口 → ExportButton → ExportPanel。单页 DESIGN/CODE 调用 /files/[id]/export/image;多页 DESIGN 调用 /files/[id]/export/images-zip;selection 仍走 /export/selection;MP4 走 /export/video。#4938 让主 PNG UI 把 scale 与 pageIds 一起交给 ExportButton。

Next.js route 先用 Zod 校验请求,再由 export/_lib/export.ts 并行物化页面和初始化临时上传。DESIGN 路径解析持久化 DesignValue,向 Go API 批量取资产/字体,把带 scale 的 render payload 发给 export worker;worker 用浏览器截图并把结果上传到 signed URL。

CODE 路径在相同的临时上传边界下分支:Go API 的 /api/export/worker-token 校验 workspace member 后签发 10 分钟 token;export worker 接收原始 HTML,在 Playwright context 级别拦截网络,按 token 从 API 解析 assets.local 和字体,图片重定向到 CDN、字体小体积代理,其他外部 HTTP/WebSocket 默认拒绝,然后按 deviceScaleFactor 截图。

ZIP 路径先取所有页面数据,再把有序 pages、资产、字体、scale 发给 worker。worker 按页串行渲染,限制 20 页和 32 MiB 累计 PNG 输入,生成稳定文件名后一次上传 ZIP。前端最后下载返回的 signed URL。

当前调用方默认值是:主单页 PNG 1×、多页 PNG ZIP 1×、MP4 1×、agent snapshot 1×;selection PNG 仍为 2×。worker 的 image、video、ZIP 合约仍保留 2× fallback,因此产品默认必须在更靠近调用方的 route/schema 层显式钉住。

关键代码#

1. UI 把 scale 变成导出请求的一部分(#4938,ExportPanel.tsx:110-126;ExportButton.tsx:89-108)。

if (format === "png" && currentPageType === "CODE" && currentPageId != null) {
  onExportPng([currentPageId], pngScale);
  return;
}
if (format === "png") onExportPng(orderedSelected, pngScale);
plaintext

设计点:同一个面板同时覆盖 CODE 单页和 DESIGN 单页/ZIP,但不让下游猜测当前 scale。#4938 的测试分别断言 DESIGN、CODE、ZIP 请求体都带 scale: 1

2. 付费门禁在 UI 侧明确表达,并对已知未知状态收敛(#4938,ExportPanel.tsx:461-513)。

const isHighResLocked = billingMe?.plan === "free";
const isEntitlementUnknown = isBillingLoading || isBillingError;
if (opt.isPaidOnly && isHighResLocked) {
  showPlanDialog({ source: "export_size", intent: "upgrade" });
  return;
}
isItemDisabled={(opt) => opt.isPaidOnly && isEntitlementUnknown}
plaintext

设计点:free 用户仍能看到能力和升级入口;billing 请求加载中或失败时不允许误选付费项。PlanDialog 增加了 export_size 来源,保留转化追踪边界。

3. 默认值下沉到调用方 route,而不是继续依赖 worker fallback(#4938,image/route.ts:8-12;images-zip/route.ts:12-16;selection/route.ts:8-16;video/route.ts:8-12)。

scale: z.number().positive().max(4).default(1)  // image / ZIP / video
scale: z.number().positive().max(4).default(2)  // selection
plaintext

设计点:route 先把产品语义固定,再把必需的 number 传入共享编排;selection 的旧 2× 高清行为被显式保留。这样可以兼容 worker 仍接受缺省 scale 的其他调用方,但也留下了多个默认源,需要后续统一策略。

4. ZIP worker 以资源隔离、串行和字节上限换取可控内存(#4626,exportImagesZip.ts:21-60)。

for (const [index, { designValue }] of pages.entries()) {
  const result = await renderImageExport({
    ...renderInput, designValue, assets: pageAssets
  });
  sourceBytes += result.data.byteLength;
  if (sourceBytes > 32 * 1024 * 1024) throw new ImagesZipTooLargeError();
}
plaintext

设计点:不并发启动多个浏览器截图,避免 Worker isolate 同时持有多个大 PNG;单页只携带自己引用的 assets/fonts,减少 payload;超过限制在上传前失败,并由 route 映射为 413。

5. CODE 导出把资源能力限制在 workspace token 内(#4706,handler/export.go:14-35;renderCode.ts:136-205)。

const exportWorkerTokenTTL = 10 * time.Minute
if _, err := h.authzService.EnsureMember(ctx, workspaceID); err != nil {
  return
}
token, err := h.workerTokenSigner.Mint(workspaceID, exportWorkerTokenTTL)
plaintext

设计点:raw HTML 无法像 DesignValue 一样预先枚举全部资源,所以 worker 需要按浏览器请求动态解析;短期、workspace-scoped token 让动态取资源仍有明确授权边界。#4706 同时在 renderCode.ts 对网络做默认拒绝,并区分字体代理与图片 CDN 重定向,降低 CORS 和 CDP 大 body 的代价。

工程取舍#

  • **契约位置:**把产品默认放在 Next.js route/schema,避免 UI 漏传时再次落到 worker 的 2× fallback;同时保留 worker fallback 以兼容 selection 或其他直接调用方。这是低风险迁移路径,但默认值目前分散在 image、ZIP、video、selection、agent tool 和 worker 合约多个地方。
  • **复用边界:**DESIGN 继续走稳定的 DesignValue + 批量资源表;CODE 不强行转成同一模型,而是复用导出上传、worker session、截图与下载链路,独立处理 HTML 资源解析。这个边界避免把 iframe/HTML 的动态资源误建模为静态资产表。
  • **性能:**ZIP 串行截图和 32 MiB 上限牺牲部分吞吐,换取 Worker 内存可预测;PNG 已压缩,ZIP 使用 level 0,避免再次为压缩付出 CPU。
  • **可靠性:**CODE 有 16,384 device-pixel 捕获上限、15 秒 capture budget 和 truncated 返回;ZIP 超限在上传前失败;短期 token 和 membership check 限制资源读取范围。
  • 测试策略:#4938 的 site 测试验证三个请求体都传 1×;#4626 测试 ZIP 顺序、资源隔离、串行启动和 32 MiB 失败;#4706 的 Go contract test 覆盖成功、未登录、非 member 和非法 workspace ID;#4837 用行为测试固定当前页 PNG 默认。本次仅做只读代码审阅,未执行测试。

和最近学习记录的关系#

今天没有选择日志中已经作为锚点的 PR;日志中今天窗口没有去重项,昨天窗口的 #4844 被跳过。最近的 #4815 学习过 export worker 如何把持久化页面物化为 cover artifact,#4598 学习过 CODE 的持久化 source 与 ephemeral DOM 边界;本次不重复 cover 或 inline-edit 代码,而是新增“导出尺度是跨调用方产品契约”和“付费能力在 route/worker 边界如何落位”的视角。

最近 #4895 与 #4844 已覆盖 agent replay、soft interrupt 和用户进度可靠性;本次只把 agent snapshot 的 1× 默认作为导出调用方之一引用,不重复 agent 流协议。

我会怎么吸收#

  1. 把隐式 fallback 当作迁移风险:先在最靠近产品语义的边界把默认值显式化,再决定底层 fallback 是否保留。
  2. 跨页面类型时复用 transport、上传和观测边界,保留 DESIGN 结构化数据与 CODE raw HTML 的不同资源解析模型。
  3. 批量渲染优先建立页数、字节、时间和并发边界,并把稳定输出命名、413 错误和不上传失败结果做成契约。
  4. 当权限信息未知时 fail closed,但不要把 UI 门禁误当服务端授权;付费能力必须在可信后端或 worker boundary 再校验。
  5. 用请求体断言和 worker contract test 验证跨层参数真的传到最终渲染器,而不是只测试面板显示。

边界、风险与未解问题#

  • **服务端 entitlement 缺口:**当前 route 只校验 scale 为正数且不超过 4,未校验 free plan;替代客户端可以直接请求 2×、3× 或 4×。PR body 已明确把 maxExportScale 类服务端强制留给后续。
  • **默认值分裂:**主 PNG/ZIP、MP4、agent snapshot 是 1×,selection 和 worker fallback 是 2×。需要确认 selection 的 2× 是否长期产品规则,并考虑把能力/默认集中到共享策略。
  • **未知 billing 状态:**代码覆盖 loading/error,但仍需确认 query 在 data 缺失而没有 isLoading/isError 的边界是否可能让 2× 可选。
  • **ZIP 资源与延迟:**串行渲染把内存压平但放大长尾;累计字节超限是在完成当前页截图后才检测,真实大图和超时场景需要运行时验证。
  • **CODE 资源失败:**字体失败会回退为空 CSS、图片解析失败返回 404;需要确认导出 UI 是否充分提示资源缺失,而不是只得到视觉降级结果。
  • overlay 交互:#4938 已知 pricing dialog 外点会连带关闭导出面板,属于全站浮层栈问题,尚未在本线解决。
  • **回归验证:**需要真实 free/pro 账号验证 1×/2× 像素、ZIP 产物、CODE 截断提示、MP4 1×,并在 agents SDK/worker 合约升级时复验 snapshot 的 scale 默认。

候选说明#

  • 候选窗口按 Australia/Melbourne 的 merge 时间计算:今天 9 条(2026-07-19T14:00Z 至 2026-07-20T14:00Z),昨天 9 条(2026-07-18T14:00Z 至 2026-07-19T14:00Z)。
  • 本地日志已有锚点 PR:#4124、#4156、#4309、#4314、#4344、#4389、#4390、#4516、#4580、#4591、#4598、#4637、#4651、#4720、#4815、#4844、#4895。今天没有去重跳过;昨天跳过 #4844。
  • 选择 #4938 是因为它同时提供产品默认、API/route 契约、worker runtime、billing gate、测试与兼容迁移的证据,并能自然连接 #4706、#4626、#4837,形成一条完整的 PNG 导出演进线。

元数据#

字段
businessLine统一 PNG 导出尺度与跨编辑器渲染边界
anchorPrNumber4938
relatedPRs4706, 4626, 4837
modulessite export UI/routes, shared export orchestration, export worker contracts/handlers, agent snapshot tool, Go worker-token API
learningTagsexport-pipeline, png-scale, billing-entitlement, route-contract, code-render, zip-bounds, worker-runtime, cross-layer-api
anchorFilesChanged12
lineStageCODE 单页出口 → 多页 ZIP 边界 → 当前页默认入口 → PNG scale 产品化与调用方默认显式化

飞书文档#

本报告创建后将把当前云文档链接回填到本地学习日志。

🗂️ 这是知识库中的🔬 研究。

内容可能仍在补充或修订中。

← Back