Voyager 业务线学习:异步 Export Job 从 PNG 基础扩成多格式 Workflow
这条业务线解决的不是“导出面板多几个格式”这么简单的问题,而是把可能超过 Go API/LB 请求时限的渲染工作移到可恢复的 Cloudflare Workflow,并建立一套能被 App 和 Platform API 共同消费的异步 job 契约
source_automation: voyager-merged-pr run_date: 2026-07-26 anchor_pr_number: 5235 pr_number: 5235 pr_title: “feat(export): run PPTX and JPG exports through Cloudflare Workflows” pr_url: https://github.com/adastralab-ai/voyager/pull/5235 ↗ author: “Zihua Li (@luin)” merged_at: “2026-07-26T08:59:17Z” modules:
- idl/typespec/services/export.tsp
- backend/go/internal/exportjob
- backend/go/apps/api/handler
- backend/workers/export/worker
- backend/workers/export/worker/workflows
- backend/workers/export/contracts
- packages/site/src/app/(main)/files/[id]/_components
- packages/site/src/app/(main)/files/[id]/_lib files_changed: 60 learning_tags:
- export-pipeline
- cloudflare-workflows
- asynchronous-job
- format-discriminator
- idempotency
- page-snapshot
- r2-state
- pptx-rendering
- jpg-encoding
- polling
- contract-codegen
- reliability
- memory-bounds business_line: “异步导出作业与跨格式渲染边界” related_prs: [5128, 5157, 5168, 4938] line_stage: “PNG 单页异步 job -> 文件/有序多页与 ZIP -> UUID 校验收敛 -> PNG job 管线统一 -> PNG scale 产品化 -> PPTX/JPG 接入 Workflow 与 App” open_questions:
- “Go、export-worker、生成客户端和部署配置必须如何协同发布,才能避免 format schema 的交错部署窗口?”
- “多页 JPG 超过 32 MiB 时如何把 ImagesZipTooLargeError 传到 job API,而不是统一暴露 workflow_failed?”
- “PPTX 在渲染前累积所有页面 designValue、assets 和 fonts 的内存上限如何验证,是否需要分页或流式 page-driver 协议?”
- “Workflow 状态和 R2 idempotency record 都是短期状态;过期、孤儿结果、重复提交和下载 URL 失效的运维语义是否足够明确?”
- “App 端的 internal variant gate 与不受该 gate 限制的 Platform API 是否需要同一套 entitlement 或流量保护?”
- “是否需要补真实 Cloudflare Workflow、Go、worker 和浏览器渲染之间的端到端回归,覆盖部署 skew、长 replay/轮询和重试竞态?” feishu_doc_url: null github_repository: joyehuang/ai-agent-field-notes github_path: outputs/voyager-daily-pr-study/2026-07-26-pr-5235-export-jobs-workflows-formats.md github_commit: 831eab365364e1a401fb7281b71759d53c7b175b
业务线概览#
这条业务线解决的不是“导出面板多几个格式”这么简单的问题,而是把可能超过 Go API/LB 请求时限的渲染工作移到可恢复的 Cloudflare Workflow,并建立一套能被 App 和 Platform API 共同消费的异步 job 契约。
用户看到的是:提交导出后拿到 job id,前端轮询,完成后下载一个临时签名 URL;单页图片直接下载,多页图片得到 ZIP,PPTX 得到一个 deck,并且 PPTX 中无法表达的内容通过 warning 告知用户。工程上真正新增的是跨边界的数据流:Go 负责身份、workspace/page 可见性和下载签名,export-worker 负责 Workflow、R2 中间态和 Browser Rendering,前端负责提交、退避轮询和降级提示。
本次学习的锚点是 #5235 ↗。它把原先以 PNG 为主的异步 export-jobs 管线扩为三个逻辑格式:png 和 jpg 共享 image Workflow,pptx 使用独立的 PPTX Workflow。它还把 format 提升为请求 discriminator、把 warnings 纳入结果契约,并将 App 的 JPG/PPTX 入口切到 job API。范围仍然有意收窄:只接受 DESIGN pages,单次最多 20 页;App 中 JPG/PPTX 暂时只在 internal variant 出现,PNG/PDF/MP4 仍处于过渡态。
今日锚点#
| 字段 | 内容 |
|---|---|
| PR | #5235 feat(export): run PPTX and JPG exports through Cloudflare Workflows ↗ |
| 作者 | Zihua Li / luin |
| merge 时间 | 2026-07-26 18:59:17 AEST / 2026-07-26T08:59:17Z |
| merge commit | 831eab365364e1a401fb7281b71759d53c7b175b |
| 变更规模 | 60 个文件,3,848 additions / 1,233 deletions |
| 选择理由 | 今日 Melbourne 窗口中唯一能把近期 Export Job 前置基础、契约、worker runtime、App 入口和测试串成一条跨层演进线的候选 |
选择它还有一个去重原因:#5235 没有作为历史锚点学习过,并且它的新问题不是重复 #4938 的 PNG scale 视角,而是回答“一个已经能跑 PNG 的异步 job,怎样安全扩到多格式、可恢复 Workflow 和真实 App 入口”。#5235 合入 main 后的 Go、IDL、Export Worker、前端质量、前端单测和浏览器测试 checks 均通过;本报告仍以合并后的真实代码为证据,不把 CI 通过等同于生产端到端已验证。
演进时间线#
| 阶段 | PR 与代码证据 | 改变的层 | 本次学习中的意义 |
|---|---|---|---|
| 已有产品前置 | #4938 ↗,既有报告 2026-07-20-pr-4938-png-export-scale.md | App 导出面板、同步图片/视频路由、Agent snapshot | 把 PNG scale 从 worker 隐式默认提升为调用方显式值;本次 #5235 继续复用 PNG 的 scale 语义,并为 JPG 增加 quality,而 PPTX 固定几何,不把图片参数带入其身份。 |
| 异步基础 | #5128 feat(export): add asynchronous image export jobs ↗,merge 266cea60dd87915012d315533efe64af4e8a3b57 | TypeSpec、Go exportjob service/client、HMAC worker API、R2 manifest、单页 PNG Workflow | 建立“创建快速返回、Workflow 执行、轮询状态、完成后签名下载”的 runtime 边界,并把 page snapshot、资源读取和渲染从同步请求中拆出来。 |
| 契约/校验收敛 | #5157 refactor(export): share export job UUID schema ↗,merge 383e57fe2d4b42e5736c605a37e1f07c931fdff0 | worker exportJobs.ts、worker entry、image Workflow | 将 worker job API、Workflow payload 和状态路由复用同一个 UUID schema,减少多个入口的规则漂移。改动小,但为后续把 format、pageIds、jobId 扩成统一 schema 先清掉重复定义。 |
| PNG job 统一 | #5168 feat(export): unify PNG export jobs ↗,merge 8c08fec0ff03b1e9f8f6dbc90e78268df82ab805 | Platform API、Go service、worker job manifest、按页 Workflow、ZIP、generated clients | 把单页 PNG 扩为 fileId 或有序 pageIds,将多页封装成 ZIP;文件目标在解析当前页面前先做 idempotency lookup,避免文件内容变化让同一个 key 失去原 job 语义。 |
| 今日锚点 | #5235 feat(export): run PPTX and JPG exports through Cloudflare Workflows ↗ | TypeSpec discriminator、session + PAT 双 API、Go 复用 service、image/PPTX 两个 Workflow、App polling/UI gate、warnings、部署 binding | 不是为每个 format 复制一套 job 系统,而是把逻辑 format 与实际渲染 pipeline 分开:PNG/JPG 映射到 image,PPTX 映射到 pptx;同时把生成结果的“可用但有缺失”作为正式状态传播到 UI。 |
这些 PR 全部是 origin/main 的祖先。#4938 已经是历史学习记录,本次将它作为前置背景,不把它重复包装成新的锚点。
当前架构与数据流#
flowchart LR
U["Site ExportPanel / ExportButton"] --> S["Session POST /api/export/jobs"]
P["Platform PAT client"] --> G["Go handler + exportjob.Service"]
S --> G
G --> C["HMAC Go -> export-worker internal job API"]
C --> I["R2 idempotency record + Workflow instance"]
I --> W1["ExportImageWorkflow: page snapshots -> render -> optional ZIP"]
I --> W2["ExportPptxWorkflow: page snapshots -> one browser deck"]
W1 --> R["R2 result object"]
W2 --> R
R --> V["Go validates pages + signs temporary URL"]
V --> Q["Site polls with backoff"]
Q --> D["downloadBlob + warning/error toast"]plaintext- 用户入口与 API 契约。
ExportPanel先依据页面类型和isPublicVariant决定可见格式;JPG/PPTX 仅在有 DESIGN page 且不是 public variant 时出现(packages/site/src/app/(main)/files/[id]/_components/ExportPanel.tsx:71-136)。ExportButton将 JPG/PPTX 统一送入downloadExportJob,PNG 的 CODE 单页、PNG ZIP、PDF、MP4 仍保留旧同步路由(packages/site/src/app/(main)/files/[id]/_components/ExportButton.tsx:114-221)。 - Go session API 与 Platform API 汇合。 session handler 先把
x-workspace-id解析并做 membership check,再解析 generated TypeSpec union;Platform handler 从 PAT identity 得到 workspace。两者都调用同一个exportjob.Service.Submit/Get,所以鉴权入口不同,job 的 page 解析、幂等和下载签名逻辑相同。 - Go service 是权限与 mutable source 的边界。
Submit校正 scale、quality、idempotency key,并将无意义的 format 参数固定成稳定值:JPG 永远transparent=false,PPTX 永远scale=1且不带透明度。fileId请求会先调用 worker lookup,再读取当前文件页面;只有 lookup miss 才解析 DESIGN page IDs。显式pageIds则直接验证每页属于 workspace 且类型为 DESIGN,然后交给 worker 创建 job(backend/go/internal/exportjob/service.go:51-127)。 - worker 负责 job 身份与 Workflow 状态。 R2 以环境、user、workspace、
Idempotency-Key的 SHA-256 作为索引;conditionalIf-None-Match写入和 ETag CAS 保证并发 claim 只有一个记录。sameExportRequest比较 format、quality、scale、transparent 和有序 page IDs;fileId请求比较的是稳定 file target,而不是第一次解析出的页面列表(backend/workers/export/worker/exportJobs.ts:327-389)。创建 Workflow 失败时,代码再get(instanceId);如果实例已经存在,就把创建竞态当作成功,避免客户端因为重复请求产生第二个 job。 - Workflow 把持久化快照和耗时渲染拆开。
pageSources.ts为每个 page 申请 workspace-scoped worker token,读取 page base content 和增量 delta,应用applyDelta,再并行批量取 assets/fonts(单批最多 200 个 ID),将完整 render sources 写进 R2 snapshot。已有合法 snapshot 会被直接复用,因此 durable step 重试不会重新读取 mutable page。 - 结果与下载。 image Workflow 为每页分别执行 prepare/render step;单页写
result.png或result.jpg,多页先写按page-001.ext命名的中间对象,再顺序读 R2 打包 ZIP。PPTX 每页只单独 materialize snapshot,随后在一个最长 10 分钟的 browser step 中合并所有页面、资源和字体,并把 deck 流式写入 R2。Go 查询 completed job 后再次检查结果中的 page IDs 属于 workspace,再签发临时 download URL;前端拿到 URL 后下载,并将 warnings 作为常驻 warning toast 展示。
关键代码#
1. 用 discriminator 让格式和可用参数在契约层分叉#
证据:#5235 diff ↗,idl/typespec/services/export.tsp:29-93。
model CreatePngExportJobRequest { format: "png"; ...ExportJobTarget; scale?: float64; transparent?: boolean; }
model CreateJpgExportJobRequest { format: "jpg"; ...ExportJobTarget; scale?: float64; quality?: int32; }
model CreatePptxExportJobRequest { format: "pptx"; ...ExportJobTarget; }
@discriminated(#{ envelope: "none", discriminatorPropertyName: "format" })
union CreateExportJobRequest { png: ..., jpg: ..., pptx: ... }plaintext代码事实:format 不再是可选字段,JPG 不接受 transparent,PPTX 不接受图片选项;三种变体在 wire 上保持 flat,而不是让公共请求对象堆满互相无效的字段。ExportJobResult.warnings 也在同一 IDL 中成为可选但结构化的结果字段。
设计点:把“格式”当作 API 身份的一部分,而不是 worker 内部的字符串分支,能让 generated Go/TS client、runtime validator 和调用方同时看到边界。代价是这是有意的 breaking change:旧的 platform 调用如果不传 format,不再默认为 PNG。
2. 在可变文件解析前保护 file-target 的幂等语义#
证据:backend/go/internal/exportjob/service.go:90-127,对应 #5168 的 fileId/idempotency diff ↗。
if input.FileID != uuid.Nil {
existing, err := s.client.Lookup(ctx, identity.UserID, workspaceID, input)
if err == nil { return existing, nil }
if !errors.Is(err, ErrJobNotFound) { return nil, ... }
}
pageIDs, err := s.resolvePageIDs(ctx, workspaceID, input)
input.PageIDs = pageIDs
job, err := s.client.Create(ctx, identity.UserID, workspaceID, input)plaintext设计点:fileId 代表“按文件当时的 DESIGN page 集合导出”,但这个集合可以在重试之间变化。因此同一个 key 必须在重新解析文件前命中原 job。pageIds 自带稳定的有序集合,直接由 worker 的 conditional R2 claim 处理即可。这是一个小但重要的边界:幂等不是简单地把请求 JSON 哈希后放在最外层,而是要先判断哪些请求字段仍然指向 mutable source。
3. 逻辑 format 映射到渲染 pipeline,而不是复制 Workflow#
证据:backend/workers/export/worker/exportJobs.ts:159-173, 226-274;#5235 diff 中对应 exportJobs.ts 的 format/Workflow hunk。
function workflowForFormat(format: ExportFormat): ExportJobWorkflow {
return format === "pptx" ? workflows.pptx : workflows.image;
}
await startWorkflow(
workflowForFormat(workflows, format),
instanceId,
format === "pptx" ? target : {
...target,
encoding: format === "jpg" ? "jpeg" : "png",
quality, scale, transparent,
},
);plaintext代码事实:png 和 jpg 共享 ExportImageWorkflow,只有 codec/quality 不同;pptx 走独立 ExportPptxWorkflow。image contract 也明确区分 job 的 format 与截图 driver 的 encoding(backend/workers/export/contracts/exportImage.ts:13-18)。
设计点:复用的是稳定的“页面快照 -> 资源解析 -> 浏览器渲染 -> R2 结果”边界,而不是强行让 PPTX 伪装成 image。新增格式的成本集中在契约变体和 pipeline mapping;真正不同的 renderer 仍有自己的 Workflow 参数和 timeout。
4. 用 R2 CAS 让 idempotency claim 和 Workflow 创建可重试#
证据:backend/workers/export/worker/exportJobs.ts:327-359。
const created = await bucket.put(key, candidateJSON, {
onlyIf: new Headers({ "If-None-Match": "*" }),
});
if (created != null) return candidate;
// 读已有 ETag;过期时用 etagMatches CAS 替换,否则比较语义请求并复用原记录。plaintext#5128 已经建立了 R2 idempotency manifest;#5168 把它扩展到 file/page target 和 ordered pages;#5235 又将 format、quality、scale、transparent 纳入 sameExportRequest。这使得相同 key 的相同请求返回同一个 job,不同 format 或 JPG quality 返回 409,而并发请求不会在 Workflow 层才发现重复。
5. 每页快照是可靠性和恢复粒度#
证据:backend/workers/export/worker/workflows/pageSources.ts:92-125、backend/workers/export/worker/workflows/exportImageWorkflow.ts:96-151、exportPptxWorkflow.ts:68-93。
for (const { pageId, pageNumber, snapshotKey } of pages) {
await step.do(`prepare image snapshot ${pageNumber}`, stepConfig, () =>
ensurePageSnapshot(this.env, snapshotKey, imageRenderSchema,
{ workspaceId, pageId }, { encoding, quality, scale, transparent }),
);
}plaintextimage pipeline 对每页分别 prepare/render;一个晚到的页面失败时,Workflow 可以从该页继续,而不必重新 materialize 前面的页面。PPTX 也为每页单独保存 snapshot,但 deck 转换本身是一个合并所有页面的单步,这是“输入可恢复、最终 renderer 不可拆分”的明确取舍。
6. 前端轮询把“暂时不可查询”和“job 失败”分开#
证据:packages/site/src/app/(main)/files/[id]/_lib/exportRequest.ts:94-161、ExportButton.tsx:57-99。
await delay(POLL_INITIAL_DELAY_MS); // 3s
for (;;) {
const job = await exportServiceGetExportJob(...);
if (job.error) {
if (++statusFailures > POLL_MAX_STATUS_FAILURES) throw ...;
} else if (job.data.status === "completed" && job.data.result) {
return job.data.result;
}
await delay(backoff(elapsed));
}plaintext代码事实:初次查询延迟 3 秒,轮询间隔在 300ms 到 5s 之间退避;最多连续 5 次状态错误才把结果判为不可知。Workflow binding 的暂时故障和真正过期的 404 对客户端不可区分,所以单次错误不应立即让用户重试。完成后,PPTX warnings 不被吞掉,而是和下载一起返回并在 UI 中常驻展示。
工程取舍#
边界与复用#
- Go 不负责渲染,也不把完整设计内容塞进长请求;它负责身份、workspace membership、DESIGN page 校验、调用 worker 的 HMAC、completed result 的 workspace 二次确认和签名 URL。
- worker 不重新实现业务授权;它只接受 Go 通过内部接口传来的 user/workspace/page 目标,并用 short-lived worker token 从 Go 读取 page、assets、fonts。HMAC 保护
/api/*,worker 的内部 route 只暴露给受信任调用方。 - R2 同时承担三种短期状态:idempotency record、每页 render snapshot、最终 result object;Workflow 的 status/output 负责 job 状态。这样不需要另建持久 job 表,但也意味着 R2 生命周期和 Workflow retention 是产品契约的一部分。
- PNG/JPG 共享 image pipeline,PPTX 保留自己的 renderer。这个复用粒度比“每个格式一套 job”小,比“所有格式一个万能 renderer”清晰。
兼容性与部署#
format从旧的“可省略、默认 PNG”变成必填 discriminator,quality只存在于 JPG,PPTX 的无效图片参数在 Go service 中被固定为稳定值。这让新契约更可判定,但旧调用方会得到 400。backend/workers/export/worker/exportJobs.ts的 internal create/lookup schema 也要求 format,因此 Go 和 worker 不能安全地任意交错部署:worker 先升级会拒绝旧 Go 请求,Go 先升级而旧 worker 忽略新字段则可能把 PPTX 当成默认 PNG 处理。当前代码把这当作受控的同批发布,而不是增加兼容层。- App 端的 JPG/PPTX gate 只在
ExportPanel中存在;Platform API 的 PAT 入口复用同一 service,却没有这个 UI variant gate。这是“未开放 App 流量”和“API 合同已存在”两件不同的事,不能把前端隐藏当作服务端授权。
性能、内存和可靠性#
- 每页独立 durable step、prepare snapshot 的幂等复用、Workflow create 后的 get fallback,以及 R2 CAS claim,共同降低了网络抖动、重试和并发重复提交的成本。
- image ZIP 在渲染过程中累计 source bytes,超过
MAX_IMAGES_ZIP_SOURCE_BYTES(现有 32 MiB 约束)就停止继续渲染;归档读取保持顺序,避免同时把所有 R2 对象读入内存。20 页上限同时出现在 TypeSpec、Go service、worker schema 和 App selector。 - page 资源按最多 200 IDs 分批,并行拉取 assets/fonts;page delta 则按 version cursor 顺序应用。这分别利用了资源 API 的批量边界和页面内容的版本顺序。
- PPTX 结果的输出通过 Browser page 的 base64 slices 和 R2 stream 写入,worker 不需要一次持有完整 deck;但
renderPptxDeck在渲染前仍把所有页面的designValue、assets、fonts 聚合到内存里。因此“输出流式”不等于“输入和 renderer 全链路流式”。 - job 与 R2 临时对象只保留约一天,完成后才签发短期 URL;这减小了存储和泄露窗口,却要求客户端及时下载,也要求运维明确过期 job、重试和孤儿对象的预期。
测试策略#
代码中有四层测试证据:
- Go session contract test 创建两页 DESIGN page,提交 PPTX,验证 202、worker payload、PPTX 固定
scale=1/transparent=false、完成后的 download URL 和 warnings,并覆盖无 format/未知 format 的 400 与非成员 404(backend/go/apps/api/contract_test/export/export_jobs_test.go:15-126)。 - worker job unit test 覆盖同请求去重、过期 key 替换、环境/user 隔离、ordered pages、format/quality 冲突、JPG 走 image Workflow、PPTX warnings、file lookup 以及 workspace/user 隔离(
backend/workers/export/worker/exportJobs.test.ts:144-421)。 - Workflow test 用 R2 fake 和 page-source API fake 验证 PNG/JPG 单页、多页 ZIP 文件名顺序、snapshot retry 复用、32 MiB 提前失败,以及 PPTX 的 per-page snapshot、单次 deck render 和 warning(
exportImageWorkflow.test.ts:103-297、exportPptxWorkflow.test.ts:80-146)。 - 前端测试验证 PPTX/JPG 走 job API、轮询临时 502、连续 404 达到阈值、下载命名和 warning toast(
ExportButton.test.tsx:222-372)。PR checks 中 Go、IDL、Export Worker、Frontend Unit Test、Frontend Quality、Frontend Browser Test 均通过。
这些测试足以证明契约映射、幂等逻辑和 fake Workflow 语义;它们还没有证明真实 Cloudflare Workflow retention/R2 conditional write、Go 与 worker 的原子部署、Browser Rendering 的重媒体内存上限或跨服务真实轮询。因此“代码路径完整”与“生产边界已压测”需要分开。
和最近学习记录的关系#
- 与 #4938 的直接关系: #4938 已把 PNG scale 变成调用方显式参数,并在 App 侧做 free/pro UI gate;#5235 把这个值纳入异步 job 的 idempotency identity,JPG 复用 scale 并新增 quality,PPTX 则固定 scale=1。前者是产品参数化,后者是把参数纳入跨服务契约和恢复模型。
- 与 #4815 的运行时关系: #4815 的 DOC cover 学习强调 render-time 资源解析、workspace-scoped worker token 和从稳定 envelope 物化渲染 artifact;#5235 的
pageSources.ts使用相同的边界思路,但将输入换成 page base content + ordered deltas,并让 snapshot 成为 Workflow 的恢复单元。这是相邻的 render infrastructure 视角,不把 cover PR 误算成本线主干。 - 与最近 Agent 线的边界: #5199 的 Durable Object reconnect/replay 和 #5143 的 Agent image-node undo 都是 Agent runtime/编辑器落地线。本次没有因为它们都涉及“异步/可靠性”就拼进 Export 业务线;只有 #5128/#5157/#5168/#4938 通过同一 export-job、schema、render worker 和用户导出流程建立了直接代码关系。
我会怎么吸收#
- 先区分逻辑格式和执行 pipeline。 设计 API 时把
format、codec 和 renderer 分成不同层;先问哪些格式真正共享 materialization/render seam,再决定是 enum mapping 还是独立 Workflow。 - 把 mutable source 转成可校验的 durable snapshot。 长任务不要在每个重试中重新读当前页面;为每个恢复单元写入 schema-validated snapshot,并让 retry 从 snapshot 继续。
- 让幂等语义靠近资源解析边界。 如果
fileId会解析出可变的页面集合,必须在解析前 lookup;如果请求已携带有序 page IDs,才可以直接对 semantic request 做 CAS claim。 - 把“可用但降级”建成结果协议。 warnings 随 result 传输并在 UI 展示,比把 PPTX 缺失内容当成功日志或直接失败更诚实;但错误也要保留足够细的 code,不能把资源上限全部折叠成
workflow_failed。 - 把部署顺序当契约设计的一部分。 有意 breaking 时,要同时更新 TypeSpec、generated clients、Go/worker schema、调用方和 rollout 方案;“旧字段会被忽略”可能比显式 400 更危险,因为它可能产出错误格式而非立即失败。
边界、风险与未解问题#
以下分为代码事实、基于代码的推断和仍需验证的项:
- 代码事实:
exportJobToAPI对所有 Workflow failed 只返回workflow_failed(backend/go/apps/api/handler/export_jobs.go:163-183)。image Workflow 会抛出ImagesZipTooLargeError(exportImageWorkflow.ts:125-130),但它不会作为可区分的 job error code 到达 UI;用户因此拿不到 PNG ZIP 已有的“减少页面数”式具体建议。 - 代码事实:
backend/workers/export/README.md:5-17仍描述“durable PNG exports”和仅有 PNG 的 Platform entry point,而wrangler.jsonc:17-32,96-110已配置 image + PPTX 两个 Workflow,TypeSpec 也已有 PNG/JPG/PPTX 三个变体。这是文档漂移,会让下一位维护者误判部署和调用边界。 - 基于代码的推断: snapshot 在 Workflow prepare step 读取 page base/deltas 后固定下来,因此用户在 job 接受之后的编辑不会改变该次导出结果。这通常是可复现导出的正确语义,但产品是否需要“导出开始前最后一次保存”以及 UI 如何提示,仍待确认。
- 基于代码的推断: PPTX 的页面/资源聚合在单次 browser render 前完成,20 页只是数量上限,不是内存上限;重媒体 deck、字体转换和 Browser Rendering 的组合可能成为 128 MiB isolate 的长尾瓶颈,需要真实数据压测。
- 仍需验证:
startWorkflow的 create/get fallback、R2 conditional write、Workflow retention 同时发生在真实 Cloudflare 环境时,是否能覆盖跨请求并发、实例刚创建但状态尚未可读、R2/Workflow 单边失败等窗口。 - 仍需验证: App 的
isPublicVariantgate 只解决入口曝光,Platform API 没有同样的产品 entitlement;如果 JPG/PPTX 有成本或容量限制,需要在 Go/worker 侧补独立授权、限流或配额,而不是依赖前端隐藏。 - 仍需验证: job 只保留一天、download URL 也短期有效;需要确认失败 job、过期 idempotency record、已完成但未下载的 R2 result 是否都由现有生命周期规则清理,是否需要监控和重试入口。
候选说明#
本次按 Australia/Melbourne 的本地日期计算当天窗口:2026-07-25T14:00:00Z 至 2026-07-26T14:00:00Z,共 8 条合入 main 的 PR:#5235、#5233、#5232、#5227、#5218、#5216、#5189、#5176。
- #5233 是 infrastructure guidance 文档整理;#5227 和 #5218 是 billing;#5216 是 CSS stacking;#5189 是 isolated agent-eval;#5232 是两文件 Agent draft-message UI 修复;#5176 是 onboarding brand guidance。它们没有像 #5235 一样通过同一 export job/runtime/schema 路径自然串起近期 2-5 个有实质关系的合入 PR。
- #5235 的前置关系可以由 #5128、#5157、#5168 的真实 diff 逐层验证,并且 #4938 提供最近的 scale 产品化背景;因此没有扩大到昨天窗口。
- 昨天窗口虽有 #5203 这类 export 变更,但它只是删除不可达 archive size check,不能补足本次跨格式 Workflow 的工程演进,也不需要为了凑数量加入。
GitHub 文档#
- 报告路径:
outputs/voyager-daily-pr-study/2026-07-26-pr-5235-export-jobs-workflows-formats.md - 锚点 PR:github.com/adastralab-ai/voyager/pull/5235 ↗
- 锚点 merge commit:
831eab365364e1a401fb7281b71759d53c7b175b - 目标仓库:
joyehuang/ai-agent-field-notes - 本次写入 commit:待推送后回填到本地学习日志;报告内容本身固定在上述目标仓库路径。
核心结论: #5235 的关键不是把 PPTX/JPG 接到一个新 endpoint,而是把 Export Job 重新分层:TypeSpec 用 discriminator 约束 format-specific options,Go 在 mutable source 前维护幂等和权限,worker 用 R2 + Workflow 管理恢复状态,page snapshot 隔离当前内容,image/PPTX 只在真正不同的 renderer 处拆分,App 再用退避轮询和 warnings 交付结果。下一步最值得补的是错误码粒度、PPTX 输入内存上限、真实 Workflow/部署 skew 回归和文档同步。