v2-refactor-temp/docs/ai/async-painting-via-jobs.md
更新日期:2026-05-31 范围:
src/renderer/aiCore/provider/custom/*(图像 transport)、src/main/data/services/PaintingService.ts、src/main/core/job/*目的:记录「aiCore 迁移到 main 之后,异步绘图改用 Job & Scheduler 实现」的设计结论与落地映射。这是迁移后的目标设计,不是当前要实现的代码——前提见文末。
用 JobManager + 一个 painting.generate JobHandler,不用 SchedulerService。
docs/references/job-and-scheduler/overview.md 区分得很清楚:
异步绘图是按需触发的后台长任务,没有 cron,所以归 JobManager。scheduler 那半边在绘图里用不上。
docs/references/job-and-scheduler/handler-authoring.md 的 §2 "Remote-poll pattern (cross-restart hand-off)" 就是异步绘图的形状:submit 到 vendor → 持久化 providerTaskId → 轮询到完成。文档明确点出了那个最关键的坑:
CRITICAL: await — without persistence the restart-recovery will re-submit the remote job, wasting user quota and producing parallel external tasks.
现有 transport 与该模式 1:1 对应,恢复所需的钩子已经存在:
dashscope/dashscopeTransport.ts:submit() 返回 { taskId },并在拿到 task id 时回调 onSubmitTaskId?.(taskId)(:404);poll(taskId, { signal })(:408)。ppio/ppioTransport.ts、aihubmix/aihubmixFlux.ts:同样是 submit → { taskId } + poll(taskId)。onSubmitTaskId 这个回调本来就是为「跨重启把 task id 交给上层持久化」而存在的——正好喂给 §2 的 ctx.patchMetadata({ providerTaskId })。
JobManager 在本仓已有成熟先例(KnowledgeService、fileProcessing/tasks/*JobHandler),不是新基建。
JobRegistry 增加类型:
declare module '@main/core/job/jobRegistry' {
interface JobRegistry {
'painting.generate': PaintingGeneratePayload
}
}
PaintingService(main,已存在)拥有它:在 onInit 里 registerHandler('painting.generate', paintingGenerateJobHandler)(注册时机见 handler-authoring「Registration timing」——必须 onInit,不能 onAllReady),并暴露一个 IPC(如 Painting_Generate)→ 内部 jobManager.enqueue('painting.generate', payload)。Handler 放 src/main/data/services/.../tasks/PaintingGenerateJobHandler.ts(handler-authoring §6 约定)。
Handler.execute = §2 remote-poll:
async execute(ctx) {
let taskId = ctx.metadata.providerTaskId as string | undefined
if (!taskId) {
const r = await transport.submit(input, { signal: ctx.signal })
if (r.imageUrls) return finalize(r.imageUrls) // 同步 vendor:一次返图,无 taskId
taskId = r.taskId!
await ctx.patchMetadata({ providerTaskId: taskId }) // CRITICAL: poll 之前先持久化
}
while (!ctx.signal.aborted) {
const urls = await transport.poll(taskId, { signal: ctx.signal })
if (urls.length) return finalize(urls)
ctx.reportProgress(/* … */)
await sleep(POLL_INTERVAL_MS, { signal: ctx.signal }) // 必须 signal-aware
}
throw new Error('AbortError: cancelled')
}
finalize 在 main 写 FileEntry + file_ref(sourceType='painting'),与已落地的 file_ref / orphan-sweep 一致。
渲染端只读观察(overview「Renderer-side consumers」规则):useJob(jobId) / useJobProgress(jobId)。渲染端不直接 enqueue / cancel,只通过 PaintingService 的 IPC 发起。这替掉了现在渲染端的轮询循环和 painting.generation.${id} 共享 cache。
dmxapi(openai-flat 族)/ ovms / silicon / aihubmix(非 async)/ openai 等是一次请求返图(submit() 返回 imageUrls 而非 taskId)。同一个 painting.generate handler 按返回的是 taskId? 还是 imageUrls? 分叉即可(见上 execute 草图),无需第二种 job 类型。这样并发、重试、可观测性对同步 vendor 同样生效,模型统一。
| 能力 | 现状(渲染端) | 用 Job 之后 |
|---|---|---|
| 跨重启续跑 | 重启即丢,task id 没了 | 持久化 providerTaskId,recovery: 'retry' 续轮询而非重交(省配额、不产生并行外部任务)—— 杀手锏 |
| 并发控制 | 无 | per-queue + 全局 globalMaxConcurrency |
| 重试 / backoff | 无 | 内建,JOB_HANDLER_THREW / JOB_HANDLER_TIMEOUT 可重试 |
| 取消 | paintingAbortControllerStore(渲染端 Map) | Job cancelRequested → ctx.signal,transport 已吃 AbortSignal |
| 进度 / 可观测性 | 临时 cache | 持久化 JobSnapshot + reportProgress |
providerTaskId 必须在第一次 poll 之前 await 持久化(§2 CRITICAL)。enqueue 带 idempotency key,防双击产生两个 vendor 任务。retry(续跑);execute 重入时必须先读 ctx.metadata.providerTaskId 决定续轮询而非重交。while (!ctx.signal.aborted) + sleep(N, { signal }),禁止 while (true) / 无 signal 的 sleep(否则取消会被拖延 N ms,见 §2 anti-pattern)。file_ref,对齐现有 file_ref / orphan-sweep。迁移后这些渲染端构件可以删掉:
paintingAbortControllerStore(取消改走 Job cancel)。painting.generation.${id} 共享 cache(状态改走 useJob / useJobProgress)。src/renderer/aiCore/provider/custom/* 的 transport 仍在渲染端(src/main/aiCore 已存在,迁移有脚手架,但 transport 尚未过去)。transport 能从 main 调用之前,这个 JobHandler 跑不起来。src/renderer/aiCore/provider/custom/__tests__/boundary/)给 PaintingGenerateJobHandler 补 request/response 契约测试。../../../docs/references/job-and-scheduler/overview.md — 架构总览、DB 驱动、Renderer 只读消费../../../docs/references/job-and-scheduler/handler-authoring.md — §2 remote-poll、注册时机、recovery × catchUp 矩阵、handler 组织约定../../../docs/references/job-and-scheduler/concurrency-and-locks.md — 并发模型../../../docs/references/ai/image-generation-parameters.md — 绘图参数化架构(transport / 出参边界)