OpenReel 桌面端 GPU 云任务(Desktop GPU Cloud Jobs)架构设计与落地解析
【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video
导读
本文基于 OpenReel Video 仓库中的设计文档 docs/superpowers/specs/2026-06-02-desktop-gpu-cloud-jobs-design.md 展开,完整讲解其设计动机、认证模型、主进程 GPU 客户端、渲染进程轮询与 AI 面板的整体方案。OpenReel Video 是一款 100% 浏览器化、无需安装与上传云端的开源视频编辑器,而本文要解决的问题是:让桌面端(Electron)应用与 iOS/Android 一样,能够使用 GPU/云渲染服务器(ai.openreel.video)执行超分(upscale)、抠像(matting)、重构图(reframe)、防抖(stabilization)、转写(transcription)、音乐生成(music-gen)等 AI 任务,并跟踪进度、把结果拉回工程。读完本文,你将掌握桌面端如何绕过 CORS 获取 GPU JWT、如何设计 token 缓存与单飞(single-flight)刷新、如何通过 IPC 暴露window.openreel.gpu.*能力,以及渲染进程轮询与产物回灌的完整数据流。
1. 背景与目标:补上桌面端缺失的一环
移动端(iOS/Android)应用已经可以正常调用 GPU/云渲染服务器提交 AI 任务,桌面端却有一个关键缺口:缺少桌面设备认证(device-attestation)原语,因此无法像移动端一样换取 GPU JWT。本文的设计目标正是补齐这个缺口,让桌面安装也能:
- 换取 GPU JWT—— 新增 Cloudflare Worker
/auth/token的plat:"desktop"支路,签发真实的短期(600 秒)ES256 JWT; - 建立云任务客户端 + 完整 AI 面板 UI—— 在编辑器中提供提交、跟踪、结果回灌的完整链路。
设计文档明确记录了与用户确认过的两项决策:
- 认证模型:在现有 Cloudflare Worker
/auth/token中新增plat:"desktop"分支,基于持久化的安装 UUID 签发短期(600s)ES256 JWT,仅受现有单次挑战(single-use challenge)+ 每实例/IP 限流约束(不做设备认证——滥用行为是"被限流"而不是"被阻止")。GPU worker无需任何改动(它忽略plat字段,设计文档引用infra/gpu-worker/core/auth.py:100验证过这一点)。 - 范围:管道(token provider + cloud-job client)加上完整的 AI 面板,暴露各类任务。
2. 为什么客户端必须放在 Electron 主进程
桌面渲染进程加载的是apps/web打包产物(app://协议)。如果渲染进程直接fetchai.openreel.video、Worker 或 R2 预签名 URL,都属于跨源请求,需要在三个服务器上为app://开放 CORS 白名单(原生移动应用不存在 CORS 问题)。
因此设计上把 GPU/broker/R2/artifact 的所有网络调用全部路由到主进程(Nodefetch,无 CORS、无 preflight),彻底绕开该问题,同时与既有 Phase 4cloud.fetch的先例保持一致。主进程还天然拥有:
- OS 密钥链(keychain):持久化安装 ID / 缓存 token;
- 原生文件系统:
readFileBytes、tempFilePath、流式writeChunk; - 对渲染进程重载的鲁棒性:主进程状态不会因页面 reload 丢失。
结论一句话概括:所有网络与 token 逻辑跑在apps/desktop主进程;渲染进程只负责驱动 UX,并通过 IPC 调用window.openreel.gpu.*。
3. 总体架构
设计文档给出了清晰的架构图:
┌── apps/web renderer (app://) ───────────────────────────────────────┐ │ AI Panel UI ── gpu-job-store (persisted) ── useGpuJobPoller (1×) │ │ │ │ │ │ │ └──────── window.openreel.gpu.* (IPC, contextBridge) ────────┘ │ ┌── apps/desktop main (Node) ──┴──────────────────────────────────────┐ │ GpuTokenProvider ──(challenge→token, cache+refresh)──► Worker /auth │ │ GpuJobClient: presign→PUT(R2)→submit→status→manifest→artifact │ │ uses native fs for source bytes + artifact temp files │ └──────────────────────────────────────────────────────────────────────┘ │ Bearer <plat:"desktop" JWT> + X-Bundle-ID: com.openreel.video ▼ ai.openreel.video (GPU worker — UNCHANGED) ▲ openreel-cloud Worker (NEW desktop leg in /auth/token)三层分工明确:渲染进程负责 UI 与轮询驱动;主进程负责 token 换取与所有网络调用;Worker 负责签发 token(新增 desktop 支路),GPU worker 保持不动。
设计文档规划共享的线上协议类型放在packages/core/src/ai/cloud-job-types.ts,渲染进程和桌面客户端都可以引用(若跨包引用不便,则按文档所述在apps/desktop中复制该小型类型文件,与cloud.ts的DIRECT_CONFIG处理方式一致)。
4. 组件 1:Workerplat:"desktop"认证支路
这是一条仅挑战(challenge-only)的支路:客户端先获取 challenge,再在没有证明(attestation proof)的情况下将其交换为 token。它复用了平台分发前已有的挑战单次使用(single-use)、限流与吊销机制。
4.1 类型拓宽(5 处)
设计文档要求把"ios"|"android"拓宽为"ios"|"android"|"desktop",共 5 处:
| 位置 | 内容 |
|---|---|
apps/cloud/src/auth/routes.ts:63 | /challenge平台校验守卫 |
apps/cloud/src/auth/kv.ts:23 | ChallengeRecord.platform |
apps/cloud/src/auth/jwt.ts:9 | JobTokenClaims.plat |
apps/cloud/src/auth/jwt.ts:37 | mintJobToken的params.platform |
apps/cloud/src/auth/routes.ts:407 | mintAndReturn的platform参数 |
4.2 新分发逻辑与处理器
在/token中(android 分支之后、invalid_platform返回之前,约routes.ts:217)插入:
if (body.platform === "desktop") return handleDesktopToken(c, kv, challengeId, challengeRecord);handleDesktopToken的核心逻辑(文档原样):
const consumed = await consumeChallenge(kv, challengeId); if (!consumed) return c.json({ error: "challenge_expired_or_used" }, 400); return mintAndReturn(c, "desktop", challengeRecord.instanceId);注意两点:无 proof 字段;instanceId来自存储的 challenge 记录,不重新发送——这避免了客户端伪造 instanceId 的可能。
4.3 外部请求/响应契约
POST /auth/challenge{ platform:"desktop", instanceId }→{ challengeId, challenge }POST /auth/token{ platform:"desktop", challengeId }→{ token, exp },JWT 特征为:ES256、iss=openreel-cloud、aud=gpu、scope=gpu:submit、plat=desktop、sub=sha256("instance:"+instanceId)、TTL 600 秒POST /auth/upload-url不变(已平台无关,只读取sub)
该支路不需要新增绑定,仅复用AUTH_KV和AUTH_SIGNING_JWK。
4.4 测试与加固预留
设计文档规划的测试(apps/cloud/src/auth/auth.test.ts)镜像 ios 分支但去掉 crypto 部分:baseEnv()+createAuthApp(),先/challenge再以{platform:"desktop"}调/token,断言 200,并用verifyJobToken(publicJwkFromPrivate(env.AUTH_SIGNING_JWK), token)验证plat==="desktop"、scope==="gpu:submit"。
加固通道(seam)在文档中记录但未实现:后续可以在handleDesktopToken内增加 proof 校验,要求一个通过带外注册的桌面密钥对签名的 challenge(复用storeAttest/readAttest),或要求账号会话。在此之前,该支路是"被限流但未被认证(rate-limited but unattested)"的。
仓库现状说明:在当前仓库快照中,
apps/下可见desktop、image、studio、web四个应用目录,apps/cloud(Worker 侧实现)未包含在内,因此上述 Worker 端改动属于设计文档规划的待实现内容,需按文档第 10 节说明部署后桌面 AI 才可用。
5. 组件 2:桌面主进程 GPU 客户端(已在仓库落地)
与设计文档高度一致,桌面端 GPU 客户端已在 apps/desktop/src/main/gpu/ 下实现,包含三个文件:token-provider.ts、job-client.ts、instance-id.ts,以及对应的单元测试job-client.test.ts。
5.1GpuTokenProvider(token-provider.ts)
实现了文档描述的全部要点:
- 安装 ID:见 instance-id.ts —— 优先从密钥链
getKeyStore().get/set("gpu-instance-id")读写(INSTANCE_ID_KEY = "gpu-instance-id"),密钥链不可用时回退到userData目录下的openreel-gpu-instance.txt文件(mode: 0o600),UUID 用randomUUID()生成一次并持久化。 getToken()缓存与刷新:REFRESH_LEEWAY_SECONDS = 60,当exp - now > 60s时直接返回缓存 token,否则走mint():POST {broker}/auth/challenge {platform:"desktop", instanceId}→POST {broker}/auth/token {platform:"desktop", challengeId}→ 缓存{token, exp}。- 单飞(single-flight):
inflight: Promise<string> | null,并发调用共享同一个mint()Promise,避免重复签发。 invalidate():清空缓存,由 GPU host 返回 401 时调用(比移动端从不自动恢复更优)。- 统一请求头:所有 broker 调用携带
Content-Type: application/json、Accept: application/json、X-Bundle-ID: com.openreel.video。
依赖注入设计(TokenProviderDeps)允许传入fetchFn与now,为单元测试中的 mock 提供了入口。
5.2GpuJobClient(job-client.ts)
完整的任务生命周期客户端,核心方法如下:
uploadMedia({ srcPath, filename, contentType })/uploadBytes(...):POST {broker}/auth/upload-url {filename, contentType}(携带 Bearer)→normalizePresign归一化双形态预签名响应 → 流式/字节PUT到预签名 URL。实现细节:PUT 请求不带 Bearer/X-Bundle-ID(直连 R2),仅携带 presign 返回的 headers,缺失 Content-Type 时才用参数补充;Uint8Array/ArrayBuffer字节处理避免多余拷贝(有对应测试断言put[1].body.buffer === bytes)。normalizePresign(纯函数):同时兼容uploadURL|putUrl、mediaKey|objectKey、getUrl|downloadURL|downloadUrl双形态,缺失必要字段时抛错。normalizePresign的别名归一化有专门测试覆盖。buildSubmitBody(纯函数):区分两种 submit 体 —— 无媒体时{request}(其中request = {kind, params});有媒体时{request, mediaKey, mediaFilename}。submitJob:POST {gpu}/jobs,携带 Bearer + X-Bundle-ID + Accept;503 时读取Retry-After头并抛出GpuRetryableError(带status与retryAfterSeconds),供上层做可重试处理。jobStatus(jobID):返回{ jobID, status, progress?, message?, manifestURL?, error?, queuePosition?, pendingAhead? }。fetchManifest(jobID):GET {gpu}/jobs/{id}/manifest。downloadArtifact(jobID, relativePath):GET {gpu}/jobs/{id}/artifacts/{relativePath},根据 Content-Type 取 mime、按扩展名生成临时文件路径(tempFilePath依赖注入),写入临时文件后返回{ tempPath, mime }—— 大文件不经过 IPC,渲染进程用fs.readFileBytes读取。cancelJob(jobID):DELETE {gpu}/jobs/{id}。- 401 自动恢复:
authedFetch在首次请求遇 401 时调用tokenProvider.invalidate()并携带新 token 重试一次,实现了文档"On any GPU-host 401: invalidate then one retry"的要求。
5.3 IPC 表面(window.openreel.gpu)
IPC 通道已定义在 apps/desktop/src/shared/channels.ts:
gpuUploadMedia: "openreel:gpu:uploadMedia" gpuUploadExport: "openreel:gpu:uploadExport" gpuSubmitJob: "openreel:gpu:submitJob" gpuJobStatus: "openreel:gpu:jobStatus" gpuFetchManifest: "openreel:gpu:fetchManifest" gpuDownloadArtifact: "openreel:gpu:downloadArtifact" gpuCancelJob: "openreel:gpu:cancelJob"对应的 zod 参数 schema 定义在 ipc-contract.ts(如gpuUploadMediaArgsSchema、gpuSubmitJobArgsSchema、gpuJobIdArgsSchema、gpuArtifactArgsSchema等),另有gpuUploadExportArgsSchema支持直接上传字节(用于导出产物场景)。结合apps/web/src/types/global.d.ts与packages/core的 bridge slice,渲染进程得到类型安全的window.openreel.gpu.*调用面。
轮询留在渲染进程(离散的jobStatus调用),镜像既有的useKieAIPoller;主进程不维护长生命周期每任务循环,避免渲染进程重载后产生孤儿循环。
5.4 桌面端测试
apps/desktop/src/main/gpu/job-client.test.ts 使用 vitest + 注入 mockfetchFn的纯单元测试方式,覆盖了:可选 download URL 别名归一化、uploadBytes零拷贝字节传递(断言 PUT body 的底层 buffer 与输入一致)。设计文档还规划了 token 缓存/刷新/单飞、三种 submit 体变体、401→invalidate→retry、503 可重试等测试场景。
6. 组件 3:渲染进程的云任务类型、store 与 poller
6.1 线上类型(packages/core/src/ai/cloud-job-types.ts)
从移动端目录移植、与线上线协议字符串逐字对齐的类型已经落地:
AI_CLOUD_JOB_KINDS常量映射,共25 种任务,snake_case 线上值(如aiHighlight: "ai_highlight"),含 transcription、ai_highlight、auto_captions、person_matting、object_tracking、face_analysis、stabilization、auto_reframe、audio_separation、color_match、colorize、upscale、scene_detection、background_removal、music_generation、photo_enhance、portrait_bokeh、smart_thumbnail、denoise、silence_removal、frame_interpolation、face_restore、object_removal、voice_enhance、translation。AiCloudJobStatus:"queued"|"uploading"|"processing"|"completed"|"failed"|"cancelled";TERMINAL_STATUSES集合与isTerminalStatus()判定终结态。MEDIA_OPTIONAL_KINDS:{music_generation, translation}—— 仅这两种任务可以不传媒体;其余全部要求mediaKey(与 workermain.py:105/387的校验一致)。- 请求/响应结构:
AiCloudJobRequest {kind; params}、AiCloudJobCreated、AiCloudJobStatusResponse(含可选的queuePosition/pendingAhead)、AiWorkerArtifactReference、AiWorkerResultManifest { jobID; kind; status?; model?; artifacts; metadata? }。 - 产物类型判定:
artifactIsImage/artifactIsVideo/artifactIsAudio合并了 iOS+Android 两套扩展名集合(图片含 jpg/jpeg/png/heic/heif/webp/tiff/tif/bmp,视频含 mp4/mov/m4v/webm,音频含 wav/m4a/mp3/aac/ogg),支持按type字段或扩展名双通道判定,还有normalizeResultManifest归一化原始 manifest。
6.2gpu-job-store(apps/web/src/stores/gpu-job-store.ts)
镜像kieai-store:持久化(key"gpu-pending-jobs"),PendingGpuJob { jobID; mediaId; projectId; kind; suggestedName; createdAt; retries; failed },动作含addJob/removeJob/incrementRetry/markFailed/retryJob/getJobsForProject,与 KieAI 相同采用 3 天过期策略。对应测试见 gpu-job-store.test.ts。
6.3useGpuJobPoller(apps/web/src/hooks/useGpuJobPoller.ts)
渲染进程唯一的轮询挂载点,在App.tsx挂载一次(桌面端仅在window.openreel?.platform==="desktop"时启用)。实现细节与文档描述完全对应:
- 递归
setTimeout:POLL_BASE_MS = 2000(2 秒基础间隔); - in-flight/timer 守卫:
timersRef与inFlightRef防止重复轮询; - 每次 tick 从 store 重新读取任务状态,避免 stale closure;
- 瞬时错误重试:
MAX_RETRIES = 5,对 HTTP 5xx/408/429 与网络错误采用指数退避(gpuBackoffMs,封顶 15 秒); - 整体 30 分钟上限:
MAX_AGE_MS = 30 * 60 * 1000,超时后markFailed并标记媒体资产错误状态(因为 worker 没有 408,这个上限由客户端强制); completed处理链:fetchManifest→ 按 kind 判定输出(outputForKind,isMultiAssetKind处理音频 stems 等多资产场景)→downloadArtifact→readFileBytes(tempPath)→ 回灌导入;failed/cancelled:markFailed并设置资产的 pending/error 标志。
值得注意的实现细节:poller 抽象了桌面端与 Web 端两套 IO(isDesktopGpuAvailable()判断),桌面端走window.openreel.gpu.*,Web 端走getWebGpuClient(),同一套轮询逻辑可复用。围绕 poller 与提交链路的配套服务包括 gpu-clip-submit.ts、gpu-result-import.ts、gpu-data-import.ts,均有对应测试。
7. 组件 4:媒体进出与 AI 面板 UI
7.1 源字节与结果回灌(packages/core/src/media)
- 输入:选中片段 →
clip.mediaId→getMediaItem→blob(reload 后为空则用loadMediaBlob重新水合)→ 桌面端通过native-media-bridge.ts导出的materializeToTemp物化为临时文件 → 把srcPath传给gpu.uploadMedia。 - 输出:
downloadArtifact得到临时路径 →readFileBytes→Blob。图片结果复用replacePlaceholderMedia;视频/音频结果走通用importMedia(file)路径(需要把目前硬编码type:"image"的replacePlaceholderMedia泛化,或非图片结果改走importMedia)。MediaItem.type保持video|audio|image不变。
7.2 AI 面板
新增顶级面板(ui-store的PanelId新增"ai",加DEFAULT_PANELS条目,在EditorInterface.tsx中按panels.ai.visible渲染网格区域,与audioMixer的实现镜像),工具栏切换,仅桌面端可见。实现文件位于 apps/web/src/components/editor/ai-panel/。
面板内容分为任务目录与提交流两部分:
任务目录分组(每类含一个小型参数表单):
| 分组 | 任务 kinds |
|---|---|
| Enhance/Restore(增强/修复) | upscale、denoise、face_restore、photo_enhance、colorize |
| Cut-out(抠图) | background_removal、person_matting、object_removal |
| Motion(运动) | stabilization、auto_reframe、frame_interpolation |
| Analyze(分析) | transcription、auto_captions、scene_detection、face_analysis、object_tracking、smart_thumbnail、ai_highlight |
| Audio(音频) | audio_separation、voice_enhance、silence_removal |
| Generate(生成) | music_generation、translation、color_match、portrait_bokeh |
大部分任务参数很少;context从选中片段填充(projectID/clipID/mediaID/renderSize/sourceDuration/sourceFrameRate/quality),按 worker 的 hoist 优先级放入params.context发送。
提交流:校验输入(除非 kind ∈MEDIA_OPTIONAL_KINDS,否则必须选中片段)→addPlaceholderMedia(pending 占位)→uploadMedia(如有媒体)→submitJob→gpu-job-store.addJob。进度/错误在资产上展示(复用isPending/kieaiError或并行的gpuError标志)+ 面板内任务列表(AIJobList.tsx)。结果成为新的媒体项,用户可拖入时间线(addClip/addClipToNewTrack)。v1 的参数表单刻意保持最小(kind + context + 少量按任务选项),面板按 kind 可扩展。
8. 错误处理与边界情况
设计文档系统性地枚举了错误处理策略,与源码实现一一对应:
- Token:单飞签发;60 秒刷新余量(
REFRESH_LEEWAY_SECONDS);401 →invalidate()+ 重试一次;broker 429 → 提示"rate limited, retry shortly"。 - Submit 503 + Retry-After:poller/submit 遵循
Retry-After,指数退避(GpuRetryableError携带retryAfterSeconds)。 - Worker 无 408:30 分钟上限由客户端强制(poller 的
MAX_AGE_MS),超时标记为 failed/timed-out。 - 输入在任务后被删(worker 会删除已上传的
mediaKey):提交后绝不重新引用。 - Artifacts TTL 24h + worker 内存态(重启后轮询 404):已知任务遇 404 视为 failed/expired;完成后应尽快下载。
- mediaKey 前缀:必须以预签名签发的
jobs/…前缀开头,始终原样使用 presign 返回的 key(normalizePresign保证这一点)。 - >150MB 视频:v1 可能跳过移动端 HEVC 预压缩(记为后续项);原生 sidecar
transcode可在将来提供该能力。
9. 代码组织与共享
- Wire 类型:packages/core/src/ai/cloud-job-types.ts 为唯一来源;桌面主进程客户端若构建允许则通过 workspace 包导入,否则在
apps/desktop复制该小型类型文件(文档化的复制策略,同cloud.ts的DIRECT_CONFIG)。 - 桌面客户端:
apps/desktop/src/main/gpu/{token-provider,job-client,instance-id}.ts+ IPC 接线(channels.ts/ipc-contract.ts/main/index.ts/preload)。 - 渲染进程:
apps/web/src/stores/gpu-job-store.ts、apps/web/src/hooks/useGpuJobPoller.ts、apps/web/src/components/editor/ai-panel/*,外加ui-store/EditorInterface/global.d.ts的编辑和packages/corebridge slice 对fs/gpu新方法的扩展。
10. 待确认事项与部署前提
设计文档明确列出构建前/中需确认的开放项:
- Broker 生产主机:iOS 用
https://api.openreel.video,web 端api-endpoints.ts用https://openreel-cloud.niiyeboah1996.workers.dev,需要选定一个作为OPENREEL_AUTH_BROKER_BASE_URL的默认值(大概率同一个 Worker)。 - 无需 CORS:所有调用经主进程,设计上已确认。
- 部署前提:Worker(
apps/cloud)改动必须部署后桌面 token 才能签发;在此之前桌面 AI 不可用。GPU worker 无需改动。 - >150MB 预压缩:推迟到后续项。
11. 分阶段实施计划
设计文档为落地计划给出了清晰的阶段划分:
- Phase A — Worker desktop leg(
apps/cloud):拓宽 5 处类型、handleDesktopToken、分发逻辑、单元测试。可独立发布与部署。 - Phase B — Desktop 主进程 GPU 客户端(
apps/desktop):token provider、job client、IPC、preload、类型;mock fetch 的单元测试。 - Phase C — 渲染进程类型 + store + poller(
packages/core+apps/web):wire 类型、gpu-job-store、useGpuJobPoller、媒体进出辅助;单元测试。 - Phase D — AI 面板 UI(
apps/web):顶级面板、任务目录、提交/进度/结果进时间线。可行处做组件测试;完整端到端渲染需要已部署的 Worker + 真实桌面运行(人工验证)。
12. 范围外内容
以下内容明确不在本次设计范围内:服务端账号/登录(账号背书的 broker 支路)、移动端改动、>150MB HEVC 预压缩、超出最小集的按任务高级参数 UI、以及任何 GPU-worker 代码改动。
总结:从 设计文档 到仓库实现,OpenReel 桌面端 GPU 云任务的方案脉络非常清晰——Worker 端只做一次"类型拓宽 + 新分发"的轻量改造,GPU worker 完全不动;桌面主进程通过GpuTokenProvider的 challenge→token、60 秒余量缓存、单飞与 401 自动恢复,以及GpuJobClient的 presign→PUT→submit→status→manifest→artifact 全生命周期管理,把跨源问题彻底消化在主进程内;渲染进程则用持久化 store + 单实例 poller + AI 面板完成 UX 闭环。该设计"既有详实实操,又有源码级实现支撑",是理解 OpenReel 桌面端 AI 能力架构的入口文档。
【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考