WuYongJie 928a7b37a9 feat(ai): use durable Relay native image tasks
Persist image submissions with fixed idempotency keys, leased execution, provider
identity, reference snapshots and staged results. Integrate DIY, assisted assembly
and non-integrated white-background sessions without holding generation in a
browser request. Recover ambiguous submissions through owner-scoped reads and
stop a new eight-view batch when restoring an earlier request.

Add the NativeImageGenerationJob migration and a dedicated worker to both release
entry points. Protect job inputs/results from storage GC. Existing production
configuration and business data are not changed by this commit.

Validation: 256 focused Jest tests, isolated PostgreSQL migration/lease/transaction
checks (8), deployment guard tests (9), type-check, production build and desktop/
mobile browser component scenarios. Live native 2K reference-image request worked;
reusing its idempotency key returned the same task. Build uses local placeholders
for otherwise absent Drawing System configuration. Three unrelated material tests
also fail with the original mainline test file; they remain outside this change.

Architecture/visual/root-cause review: passed with warnings. Independent review
completed for the integration and shared input/storage paths; worker additionally
reviewed by the primary agent. Production merge/deployment requires authorization.
2026-10-15 14:55:46 +08:00

24 lines
1.2 KiB
JavaScript

// Dedicated process: it resumes persisted jobs after application/host restarts.
const secret = process.env.AI_IMAGE_WORKER_SECRET?.trim()
const port = Number(process.env.PORT ?? process.env.APP_PORT ?? 3001)
if (!secret || !Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error('AI image worker requires AI_IMAGE_WORKER_SECRET and a valid local app port')
}
let stopping = false
process.on('SIGINT', () => { stopping = true })
process.on('SIGTERM', () => { stopping = true })
while (!stopping) {
let idle = false
try {
const response = await fetch(`http://127.0.0.1:${port}/api/internal/ai-image/worker/run`, {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(120_000),
headers: { 'x-ai-image-worker-secret': secret },
})
if (!response.ok) throw new Error('worker turn failed')
const payload = await response.json()
idle = payload?.data?.worker?.status === 'idle'
if (payload?.data?.worker?.status === 'blocked') console.error('[AI image worker] a task requires review')
} catch { console.error('[AI image worker] turn unavailable; persisted tasks will resume') }
if (!stopping) await new Promise(resolve => setTimeout(resolve, idle ? 5000 : 1000))
}