Voyager 业务线学习:Export Job 的 durable Workflow 收口与跨层数据流
这条业务线解决的是“导出同一种设计时,为什么不同入口有不同的超限、失败、内存和重试语义”。在本次演进前,多页 PNG、选区图片和 PDF 分别存在同步路由或独立 Worker handler;一部分格式已经进入 Cloudflare Workflow,另一部分仍在 Next.js/Worker 请求生命周期内完成…
source_automation: voyager-merged-pr run_date: 2026-08-06 anchor_pr_number: 5683 pr_number: 5683 pr_title: “refactor(export): simplify export job data flow” pr_url: https://github.com/adastralab-ai/voyager/pull/5683 ↗ author: “luin” merged_at: “2026-08-06T03:24:55Z” modules:
- idl/typespec/services/export.tsp
- backend/go/apps/api/handler/export_jobs.go
- backend/go/internal/exportjob
- backend/workers/export/worker
- backend/workers/export/worker/workflows
- packages/site/src/app/(main)/files/[id]/_components/ExportButton.tsx
- packages/site/src/app/(main)/files/[id]/_lib/exportRequest.ts files_changed: 9 changed_files:
- backend/go/internal/exportjob/client.go
- backend/go/internal/exportjob/client_test.go
- backend/go/internal/exportjob/model.go
- backend/go/internal/exportjob/service.go
- backend/go/internal/exportjob/service_test.go
- backend/workers/export/worker/workflows/exportImageWorkflow.test.ts
- backend/workers/export/worker/workflows/exportImageWorkflow.ts
- backend/workers/export/worker/workflows/exportPdfWorkflow.ts
- idl/typespec/services/export.tsp learning_tags:
- export-pipeline
- cloudflare-workflows
- durable-workflow
- export-job-contract
- normalized-input
- immutable-page-source
- backpressure
- memory-bounds
- api-compatibility
- test-strategy business_line: “统一 Export Job 的 durable Workflow 管线、跨层契约与数据流” related_prs: [5580, 5600, 5668, 5686, 5687] related_prior_prs: [4938, 5235, 5320] line_stage: “JPG/PPTX 进入 Workflow (#5235) -> 多页 PNG 统一 items 与 page source (#5580) -> 删除同步多图分叉 (#5600) -> PDF 接入 durable job (#5668) -> Go/Worker/TypeSpec 收敛为规范化输入 (#5683) -> DESIGN 单页 PNG 收口 (#5686) -> 删除旧 Stage 渲染链 (#5687)” open_questions:
- “exportJobs.ts 通过同一个 instance id 依次探测 image、pdf、pptx Workflow;格式继续增加时查询 fan-out、错误分类和观测成本会扩大。”
- “#5683 把文件名冲突校验从 Worker 删除并前移到 Go service;如果未来存在绕过 Go 的受信调用方,内部 endpoint 与公共校验会重新漂移。”
- “Workflow 成功/失败 retention 为 1 day,但 page source、逐页结果和 multipart 临时对象的 R2 清理策略不在本线代码中,需确认生命周期与成本边界。”
- “PDF 多页合并逐个读取 R2,限制了输入并发,但最终 PDFDocument 仍随页数增长;100 页上限不等于有明确的字节或内存上限。”
- “公共 Idempotency-Key 已标记 deprecated 且被忽略;创建响应丢失后的客户端重试可能产生重复 export job,需确认是否由上层调用方接受。”
- “单测覆盖本地 Workflow step、Go contract 和 UI polling,但没有 Cloudflare Workflow、R2、浏览器渲染和滚动发布 skew 的部署级端到端证明。” feishu_doc_url: null github_repository: joyehuang/ai-agent-field-notes github_path: outputs/voyager-daily-pr-study/2026-08-06-pr-5683-export-workflow-unification.md voyager_merge_commit: 497b25ed10a789afd090b842cfbfbc532f1800cb voyager_origin_main_snapshot: 8543dac6d775632acf8694660bea1f0269c37599
业务线概览#
这条业务线解决的是“导出同一种设计时,为什么不同入口有不同的超限、失败、内存和重试语义”。在本次演进前,多页 PNG、选区图片和 PDF 分别存在同步路由或独立 Worker handler;一部分格式已经进入 Cloudflare Workflow,另一部分仍在 Next.js/Worker 请求生命周期内完成。结果是:页数上限分叉、同步 ZIP 有输入内存上限、资源缺失有不同错误码、同一个页面资源会被不同路径重复物化。
当前范围已经收敛为:
- DESIGN 的 PNG/JPG/PDF/PPTX 通过
POST /api/export/jobs创建 durable job,再由页面轮询并下载短期签名 URL; - 图片输入在 App API 中是
items,每个 item 可以是整页或同页节点选区;PDF/PPTX 和 Platform API 继续使用有序pageIds; - Worker 用
image、pdf、pptx三类 Workflow 复用同一页源物化与任务状态边界; - CODE PNG 和 MP4 仍保留同步渲染,因为 CODE 页不能直接复用 DESIGN 的 page source;旧 Stage export 路径则已删除。
今日锚点 #5683 看起来是一个小型 refactor,实际改变的是跨 Go service、Worker 参数、TypeSpec 变体和 Workflow output 的“规范化输入”边界。它把“已从文件权限检查中解析出的值”和“原始请求值”合并为一套内部字段,降低新增格式继续复制状态的成本。
下文区分代码事实、基于代码的推断和仍待确认的问题。所有正式关联 PR 的 merge commit 都已通过 git merge-base --is-ancestor <sha> origin/main 验证。
今日锚点#
| 项目 | 内容 |
|---|---|
| PR | #5683 refactor(export): simplify export job data flow ↗ |
| 作者 | luin |
| 合并时间 | 2026-08-06 13:24:55(Australia/Melbourne,03:24:55Z) |
| Voyager merge commit | 497b25ed10a789afd090b842cfbfbc532f1800cb |
| origin/main 快照 | 8543dac6d775632acf8694660bea1f0269c37599(#5687) |
| 文件数 | 9 |
| 选择理由 | #5683 是 Melbourne 当天已合入、尚未作为锚点学习的跨层枢纽;它能自然串起昨天的多页 PNG/同步路径迁移、今天的 PDF 和 DESIGN 单页 PNG 收口。相邻的 #5687 主要是删除旧链路,单独作为锚点无法讲完整的用户流程。 |
演进时间线#
| 阶段 | PR 与真实代码变化 | 改变的层 | 新边界 |
|---|---|---|---|
| 既有 Workflow 基础 | #5235 ↗ 将 JPG/PPTX 放入 export-jobs;本次作为最近学习记录中的前置背景,不计入正式 related 集合。 | Go export job、Worker Workflow、UI polling | 先建立持久化 job 和统一下载结果的边界,但 PNG/PDF 仍有同步分支。 |
| 多页 PNG 进入 Workflow | #5580 ↗,merge 33092d5d12d7b97ca4d32a61cb01aece32c9c14e。App PNG 从 pageIds 改为 items,上限统一为 100;Worker 按页建立 R2 page source,再按 item 渲染;缺失资源用 NonRetryableError 和 export_assets_unavailable 终止;同步 images-zip、selection 路由删除。 | TypeSpec、Go validation、export Worker、site export API | 同页多个选区共享一次 page/API 物化,重试固定在同一页版本;失败不会白烧 3 次 Workflow render retry。 |
| 删除同步多图分叉 | #5600 ↗,merge 8f6075776187aa9db9b50f468c6405817073a862。删除 executeImagesExport、/api/export-images 的多图 handler 和同步 ZIP 的 32 MiB source cap;选区图片下载改为创建 image job。 | Worker handler、Workflow、site hooks/UI | 多页和选区不再依赖一次 HTTP 请求的内存与执行时长;同步路径只留下还没有 durable 输入模型的场景。 |
| PDF 接入同一 durable job | #5668 ↗,merge e071046ca19d81ae1a65fecd9ad083043039f2c0。新增 ExportPdfWorkflow,删除同步 PDF route/handler;每页先写 R2,多个 PDF 按请求顺序逐个合并,单页直接写最终结果。 | TypeSpec、Go job client、Worker Workflow、site panel | PDF 与 PNG/JPG 共用异步创建/查询/下载契约;页面渲染并发受控,合并输入不一次性全部读入。 |
| 今日锚点:规范化内部输入 | #5683 ↗,merge 497b25ed10a789afd090b842cfbfbc532f1800cb。删除 CreateInput.ResolvedItems / ResolvedPageIDs,Go service 在权限和页面校验后直接回写 Items / PageIDs;TypeSpec 把 PNG/JPG options 提成 aliases;Workflow archive/merge step 只负责副作用,最终 output 由 run 统一返回。 | Go model/service/client、TypeSpec、Workflow output | 从“raw + resolved”两套内部状态收敛到一套 worker-ready input,同时不改变已经发布的 Platform page-list wire shape。 |
| DESIGN 单页 PNG 收口 | #5686 ↗,merge b077fcae3c62007b7a5de93b5ab8f46e1767464f。ExportButton 不再把 DESIGN 单页 PNG 送同步 route;/export/images 只保留 CODE PNG,DESIGN 单页和多页都调用 export job。 | site export UI/lib、CODE route | 用户看到的 DESIGN PNG 不再因页数选择另一套资源失败和错误提示;CODE 是明确保留的例外。 |
| 删除遗留 Stage 渲染链 | #5687 ↗,merge 8543dac6d775632acf8694660bea1f0269c37599。移除 stage.ts contract、handler、stage.html、StageRenderer、dev script 和 @repo/stage-entities 依赖;/api/stage-export 返回路径不再注册。 | export Worker render/contract/package | 已无仓库内或组织内调用方的旧兼容链不再和 Workflow renderer 并存;外部未声明调用方仍是发布风险。 |
这条线不是把所有格式强行做成同一套渲染器,而是统一 job 生命周期、资源物化和结果下载;图片、PDF、PPTX 仍各自有符合格式的 Workflow。同步例外被收窄为 CODE PNG/MP4,并在 UI 和 route 中显式表达。
当前架构与数据流#
flowchart LR
U["ExportButton / selection hook"] --> S["downloadExportJob + poll"]
S --> A["POST /api/export/jobs"]
A --> G["Go handler + exportjob.Service"]
G -->|"App image items"| W["internal export-jobs"]
G -->|"Platform/PDF/PPTX pageIds"| W
W --> I["image Workflow"]
W --> P["pdf / pptx Workflow"]
I --> R["R2 immutable page source"]
P --> R
R --> B["browser render steps"]
B --> O["R2 result / ZIP / PDF"]
S --> Q["GET job status"]
Q --> D["Go rechecks access + signs URL"]
C["CODE PNG"] --> X["synchronous export-code-image"]plaintext- App UI 的
ExportButton将 DESIGN PNG/JPG 转成{ format, fileId, items, scale },PDF/PPTX 转成{ format, fileId, pageIds },统一调用downloadExportJob。packages/site/src/app/(main)/files/[id]/_components/ExportButton.tsx:145-253。 exportRequest.ts创建 job 后先等待 3 秒,再以 300ms 到 5 秒的递增间隔轮询;连续 5 次 status request failure 才认为状态不可知。Workflow 仍运行时,客户端不会硬编码总超时去伪造失败。packages/site/src/app/(main)/files/[id]/_lib/exportRequest.ts:104-175。- Go handler 依据
format解联合体:App PNG/JPG 的items变为exportjob.ExportItem,PDF/PPTX 的pageIds保持页面列表;workspace member、file/page 类型和上限在 job 创建前校验。backend/go/apps/api/handler/export_jobs.go:80-145。 - Go service 在 worker 调用前做默认值和边界检查,并读取一次文件权限快照。若输入已经是
Items,只校验每个 page;若是 page list,则按文件中的 page order 得到PageIDs,平台图片再展开为 whole-pageItems。backend/go/internal/exportjob/service.go:48-114。 - Worker 的内部 HMAC endpoint 用 Zod 再验证 discriminated union。
items走同一个 image Workflow,pageIds通过workflows[request.format]选择 PDF 或 PPTX;每个 Workflow instance 保留成功/失败状态 1 天。backend/workers/export/worker/exportJobs.ts:56-145,165-197。 - image Workflow 为每个不同 page 建一个
sources/page-NNN.json,其中保存 materialized design value、所需 assets 和 fonts;prepare 最多 10 个并发,render 最多 3 个并发。多个 item 只在 render 阶段做 node selection,最后用 R2 multipart 逐项写 ZIP。backend/workers/export/worker/workflows/exportImageWorkflow.ts:93-167、backend/workers/export/worker/workflows/pageSources.ts:130-221。 - PDF Workflow 也先准备 page source,再每页写 PDF;多页才进入
merge pdf documentstep,并按请求顺序逐个从 R2 读入pdf-lib。单页直接使用result.pdf,避免无意义的中间页对象。backend/workers/export/worker/workflows/exportPdfWorkflow.ts:58-109,147-165。 - 查询完成 job 时,Go 重新读取结果文件并检查 workspace/page 仍可见,然后才为
objectKey签发 URL;Worker 的任意失败默认映射为workflow_failed,只有明确的export_assets_unavailable对外保留。backend/go/internal/exportjob/service.go:262-314、backend/go/apps/api/handler/export_jobs.go:268-292。
关键代码#
1. #5683:raw request 与 resolved request 合为一套内部模型#
位置:PR #5683 diff,backend/go/internal/exportjob/model.go;当前上下文 backend/go/internal/exportjob/service.go:87-109。
type CreateInput struct {
- Items []ExportItem
- PageIDs []uuid.UUID
- ResolvedItems []ExportItem
- ResolvedPageIDs []uuid.UUID
+ // Platform image requests arrive as PageIDs and are normalized to Items
+ // before the worker client receives this input.
+ Items []ExportItem
+ PageIDs []uuid.UUID
Format Format
}plaintext代码事实是:Go service 仍先做 workspace/file/page 校验;改变只在校验后的内部传递方式。#5683 同步更新 client.go,PDF/PPTX 发送 input.PageIDs,图片发送 input.Items,不再存在“调用方原始值”和“worker 实际值”两套字段。合理推断是新增格式时只需要维护一次 normalization,而不是继续增加 Resolved* 对。
2. #5683:TypeSpec 复用 options,但保留 App/Platform wire boundary#
位置:idl/typespec/services/export.tsp:55-169;PR #5683 diff。
alias PngExportJobOptions = {
scale?: float64;
transparent?: boolean;
};
model CreatePngExportJobRequest {
format: "png";
...ImageExportJobTarget;
...PngExportJobOptions;
}
model PlatformCreatePngExportJobRequest {
format: "png";
...WorkflowExportJobTarget;
...PngExportJobOptions;
}plaintext这不是把两种调用方强行合并:App 图片需要 items,Platform 仍需要 pageIds;共享的是 options 校验和生成逻辑。PR diff 没有改 idl/openapispecs/api.yml,说明这次 refactor 的目标是源模型去重和内部数据流收口,而不是破坏已发布的 Platform contract。
3. #5580/#5600:先固定 page source,再并行 item render#
位置:backend/workers/export/worker/workflows/exportImageWorkflow.ts:110-167、backend/workers/export/worker/workflows/pageSources.ts:130-221。
await pMap(
pages,
({ pageId, pageItems, pageNumber, sourceKey }) =>
step.do(`prepare image page ${pageNumber}`, stepConfig, () =>
ensurePageSource(this.env, sourceKey, {
workspaceId,
pageId,
items: pageItems.map(({ nodeIds }) => ({ nodeIds })),
}),
),
{ concurrency: MAX_CONCURRENT_PREPARE_STEPS },
);
await pMap(
workflowItems,
({ pageId, itemNumber, objectKey, nodeIds }) =>
step.do(`render image ${itemNumber}`, stepConfig, () =>
renderImageFromSource(this.env, sourceKeysByPageId.get(pageId)!, objectKey, nodeIds, options),
),
{ concurrency: MAX_CONCURRENT_RENDER_STEPS },
);plaintext设计点有两个:同页多个 selection 只做一次 token/page/assets/fonts 物化;render retry 读取不变的 R2 source,而不是重新从 Go API 取一个可能已经变化的页面。#5600 删除旧的 executeImagesExport 后,这个边界也成为多图 ZIP 的唯一实现。测试在 exportImageWorkflow.test.ts:206-251 断言同页资源请求只发生一次,在 :299-360 断言已有 source 时不再请求 API、某个 item 失败后仍复用 source。
4. #5668:PDF 将并发渲染和顺序合并拆成不同的 step#
位置:backend/workers/export/worker/workflows/exportPdfWorkflow.ts:64-109,147-165。
await pMap(pages, ({ pageNumber, sourceKey }) =>
step.do(`render pdf page ${pageNumber}`, stepConfig, () =>
renderPdfPage(this.env, sourceKey, pdfObjectKey),
),
{ concurrency: MAX_CONCURRENT_RENDER_STEPS },
);
if (shouldMerge) {
await step.do("merge pdf document", stepConfig, () =>
mergePdfPages(this.env.EXPORT_BUCKET, pageObjectKeys, resultObjectKey),
);
}
return { objectKey: resultObjectKey, fileId, pageIds };plaintext代码事实是多页 PDF 的 page render 可以并发,merge 则按请求顺序逐个读取 R2;单页不创建 pages/page-NNN.pdf 中间对象。测试 exportPdfWorkflow.test.ts:93-148 用不同页面尺寸验证顺序,:150-166 验证单页直写。合理推断是把“可并发的浏览器工作”和“必须有序的文档组装”拆开,降低峰值输入内存和重试耦合。
5. #5683:Worker 选择 pipeline,外部格式不再复制分支#
位置:backend/workers/export/worker/exportJobs.ts:165-213。
await span({ name: "start workflow", op: "export.submit" }, () => {
if ("pageIds" in request) {
return startWorkflow(workflows[request.format], instanceId, {
...target,
pageIds: request.pageIds,
});
}
return startWorkflow(workflows.image, instanceId, {
...target,
items: request.items,
encoding: request.format === "jpg" ? "jpeg" : "png",
quality: request.quality,
scale: request.scale,
transparent: request.transparent,
});
});plaintextpng/jpg 的差异在 encoding 和 options,复用 image Workflow;pdf/pptx 的差异在 Workflow binding,复用 document params。状态查询因为 job id 不编码 format,按 image、pdf、pptx 依次探测同一 instance id;这是一种小而明确的兼容设计,也留下了格式增加后的 fan-out 问题。
6. #5686:同步例外在用户入口处显式收窄#
位置:PR #5686 diff,packages/site/src/app/(main)/files/[id]/_components/ExportButton.tsx:178-204。
// CODE pages cannot use the durable pipeline, so their PNG capture stays
// synchronous while DESIGN pages use the same job flow at every page count.
if (selectedPageId && isCodePage) {
void runRequest("/files/${fileId}/export/images", { ... });
return;
}
exportImageJob("png", pageIds, scale);plaintext这段分支证明“统一 Workflow”不是无条件重写所有导出:CODE 的 HTML screenshot 仍依赖 workspace-scoped token 和同步 route;只有 DESIGN PNG 取消单页 shortcut。对应 route 的测试不再测试 DESIGN worker 资源预取,而测试改为 CODE page 的 worker token 和 export-code-image 请求。
工程取舍#
边界与复用#
- 可信边界在 Go service:它先做 workspace membership、file ownership、DESIGN page 类型和 100 item/page 上限,再调用内部 Worker。Worker 仍做 HMAC 和 Zod 校验,但不重新承担文件权限语义。
- 复用发生在正确的层:
pageSources.ts复用 page/version/resource 物化;image、PDF、PPTX 保留不同的最终 artifact 组装;App 与 Platform 保留不同 target model。这样减少重复,又没有把 ZIP、PDF merge 和 deck render 伪装成同一业务。 - #5683 删除 Worker 侧 image file-name collision check,把 caller error 前移为 Go 400。收益是单一校验源,代价是内部 endpoint 对调用方契约更敏感。
兼容性与发布#
- TypeSpec 维持 discriminated union 的平面 wire shape;Platform PNG/JPG 的 page-list 请求仍由 Go 展开成 whole-page item,外部 Platform SDK 不需要改成 App 的 item contract。
- App API 新增 PDF 变体,删除同步多图/PDF 路由属于站内调用方迁移;#5686 还明确保留 CODE PNG route,避免把 CODE 的资源模型强行塞进 DESIGN Workflow。
- #5580 的发布说明承认旧/新 Go/site 短暂共存时,创建请求可能触发一次
CLIENT_OUTDATEDreload;这需要发布流程和真实部署验证,而不是只看生成的 TypeScript client 能否编译。
性能与可靠性#
- Workflow 将 prepare 并发限制为 10、browser render 限制为 3;同页 selection 共享 source;ZIP 使用 R2 multipart 和 5 MiB part buffer,避免把所有渲染结果一次性装进 Worker 内存。
- 缺失 asset 由
NonRetryableError直接变成export_assets_unavailable,前端显示“重试或移除图片”;未知失败收敛为workflow_failed,避免把内部错误文案变成公开 API vocabulary。 - PDF 多页中间页落 R2,按顺序读入 merge;单页直接写 result。这降低同步请求的内存/时长压力,但最终 PDF 库对象仍需内存,不能把 100 页当成无限安全阈值。
- Job 创建与 status 查询由 Workflow instance 负责持久化;客户端只做有限的 status fault tolerance,不自行判断长任务超时,避免生成完成但前端已把它标成失败。
测试策略#
- Worker workflow test 使用真实的本地 bucket/step harness,mock 浏览器和 API,验证 page source 重用、selection 裁剪、ZIP entry naming、缺失 asset non-retryable、PDF 页序和单页直写。
- Go service/handler/contract test 验证 100 页边界、App items 透传、Platform page list 展开、PDF 请求不携带图片 options,以及完成后重新检查页面访问权。
- Site test 验证 DESIGN 单页 PNG、多页 PNG ZIP、PDF、PPTX、JPG、超过 20 页、status transient failure 和
export_assets_unavailable的用户提示;CODE PNG 仍验证同步 route。 - 尚未被这些测试证明的是真实 Cloudflare Workflow retry semantics、R2 multipart 大对象、真实 browser/render worker、Go/site rolling skew、PDF 超大结果和生产端 R2 清理。
和最近学习记录的关系#
- #4938 学到的是 PNG scale 从 Worker fallback 提升为调用方显式 contract;本线继续把 scale/quality/transparent 作为 TypeSpec variant options,并在 Go service 按 format 只接受能被该格式消费的字段。
- #5235 学到的是 JPG/PPTX 先进入 export-jobs;本次把多页 PNG、PDF 和 DESIGN 单页 PNG 继续搬入同一 job lifecycle,完成“格式接入”到“数据流收口”的阶段转换。
- #5320 学到的是 image Workflow 的 prepare/render 并行化;本次的 page source 复用和 10/3 concurrency 保留这个性能方向,同时把 retry 输入固定成 R2 source。
- 与昨天 #5591 的图片参考图业务线不同,本次不把 Agent image refs 作为 related PR;只有 #5580/#5600/#5668/#5686/#5687 在 export job、page source、同步/异步边界上有直接代码关系。
我会怎么吸收#
- 在权限和 schema 校验完成后立即把多种外部请求归一化为一套内部模型;不要同时携带 raw/resolved 两份状态。
- 对可重试的长任务,先持久化不可变输入,再并行执行副作用;render retry 不应重新读取会变化的业务源。
- 共享生命周期和资源物化边界,保留格式特有的 artifact 组装;“统一管线”不等于“统一所有渲染实现”。
- 将业务上不可重试的资源缺失编码为稳定错误码,把其他内部失败压缩为受控的公开 vocabulary。
- 只保留有明确运行时理由的同步例外,并在用户入口、route、测试和文档中同时标出例外边界。
边界、风险与未解问题#
已由代码确认#
- App image requests 使用
items,Platform image requests 使用pageIds;Go service 在发送到 Worker 前把 Platform 图片展开为 whole-page items。 - image/PDF Workflow 都先经过 page source prepare;image 对同页多个 selection 只请求一次页面、asset、font 数据;PDF 单页不做 merge。
#5683删除ResolvedItems/ResolvedPageIDs,但保留了公共 TypeSpec union、Go 权限检查和 Worker-ready 的 Items/PageIDs 分流。#5686之后/files/[id]/export/images只服务 CODE PNG;DESIGN PNG(单页和多页)从ExportButton进入 job polling。#5687删除旧 Stage contract、handler、renderer、HTML、dev script 和 package dependency;当前worker.ts不再注册/api/stage-export。
合理推断#
- page source 将资源解析从每个 item 提升到每个 page,主要收益是同页 selection 的网络和权限读取去重;代价是一个页面任一资源缺失会使该页所有 item 一起失败。
startWorkflow在 create 报错后尝试get(instanceId),是在没有真正使用 Idempotency-Key 的情况下用确定性 instance id 做一次有限的重复创建恢复;它不等价于完整的跨客户端幂等协议。- 用 job id 探测三个 Workflow binding 简化了公开 status contract,但把“格式路由”从标识符中拿掉,未来格式数量增加会放大 status latency 和错误诊断复杂度。
尚待确认#
- Workflow retention 到期、失败中间对象和 multipart abort 后的 R2 对象是否由统一 lifecycle policy 清理;当前 diff 没有给出存储回收证明。
- #5683 之后如果有非 Go 的内部调用方直接调用
/api/internal/export-jobs,文件名默认冲突、file/page authorization 和 public error mapping 是否仍与 Go service 完全一致。 - 多页 PDF 在真实 100 页、大图片和异常字体情况下的
pdf-lib峰值内存、Workflow step timeout 和最终结果大小上限。 - Cloudflare production 中
pMap并发、Workflow step retry、浏览器 binding 与 R2 consistency 是否保持单测假设;尤其是页面 delta 在 source materialization 期间继续写入的竞态。 - 发布期间旧/新 generated API client、Go handler 和 Worker schema 短暂共存时,哪些格式会触发 reload/502;需要 deployment-level contract smoke test。
Idempotency-Key被 schema 标为 deprecated 但忽略,创建请求在网络超时后是否允许重复扣取资源/占用 Workflow 配额,仍需产品级决策。
候选说明#
候选窗口使用 Australia/Melbourne 2026-08-06 当天的 merged PR;没有扩大到更早日期寻找新的锚点。当天有多条 export 变更,#5687 是删除旧 Stage 路径的收尾,#5686 是 DESIGN PNG 的入口收口,#5683 则同时触及 Go service、Worker params、TypeSpec 和 Workflow output,且能自然连接 8 月 5 日的 #5580/#5600 与 8 月 6 日的 #5668。因此选择 #5683 作为学习锚点,而不是把同日 cleanup PR 拼成主线。
正式关联集合只包含有代码证据的同一条 export job 线:#5580 的 page source/items 契约是 #5600 的前提;#5600 与 #5668 都删除同步格式路径并复用 Workflow 生命周期;#5683 收敛它们共享的输入/output;#5686 依赖这一 job contract 收口 DESIGN PNG;#5687 删除已被新管线取代的旧 renderer。#4938、#5235、#5320 仅作为最近学习记录中的前置背景,不冒充本次正式 related PR。
验证记录:
- Voyager 初始和报告生成前状态均为
jh-vision-outsource-eval...origin/main [ahead 10, behind 427],未修改工作区;远端刷新使用git fetch --prune origin。 - Voyager 工作区没有运行格式化、代码生成、安装或测试命令;代码证据来自 PR merge commit 的
git show和origin/main文件上下文。 - #5580
33092d5d12d7b97ca4d32a61cb01aece32c9c14e、#56008f6075776187aa9db9b50f468c6405817073a862、#5668e071046ca19d81ae1a65fecd9ad083043039f2c0、#5683497b25ed10a789afd090b842cfbfbc532f1800cb、#5686b077fcae3c62007b7a5de93b5ab8f46e1767464f、#56878543dac6d775632acf8694660bea1f0269c37599均通过git merge-base --is-ancestor <sha> origin/main。 - 目标仓库在写入前已从
origin/mainfast-forward 且工作区干净;本报告和索引是本次唯一待提交的目标仓库改动。
GitHub 文档#
- 报告文件:
outputs/voyager-daily-pr-study/2026-08-06-pr-5683-export-workflow-unification.md - 锚点 PR:https://github.com/adastralab-ai/voyager/pull/5683 ↗
- 关联 PR:5580 ↗、5600 ↗、5668 ↗、5686 ↗、5687 ↗
- Voyager 锚点 commit:
497b25ed10a789afd090b842cfbfbc532f1800cb - Voyager origin/main snapshot:
8543dac6d775632acf8694660bea1f0269c37599 - 目标仓库:
joyehuang/ai-agent-field-notes;本次推送后的目标仓库 commit 会写入外部study-log.jsonl。
本报告的持久化来源只有 GitHub Markdown;metadata 中的 feishu_doc_url 固定为 null。