@
Build backend images / build content-svc (push) Failing after 19s
Build backend images / build file-svc (push) Failing after 1m53s
Build backend images / build gateway (push) Failing after 16s
Build backend images / build identity-svc (push) Failing after 7m1s
Build backend images / build notification-svc (push) Failing after 7m24s
Build backend images / build render-svc (push) Failing after 3m12s
Build backend images / build studio-svc (push) Failing after 43s
Build backend images / build content-svc (push) Failing after 19s
Build backend images / build file-svc (push) Failing after 1m53s
Build backend images / build gateway (push) Failing after 16s
Build backend images / build identity-svc (push) Failing after 7m1s
Build backend images / build notification-svc (push) Failing after 7m24s
Build backend images / build render-svc (push) Failing after 3m12s
Build backend images / build studio-svc (push) Failing after 43s
feat: AE template scanner + scene editor + AEP bundle pipeline
Scene editor (admin): per-project Scenes / Shared Colors / Color Presets
manager (ProjectScenes) reachable from each project.
AEP bundle pipeline: upload .aep or .zip → stored once per template at
templates/{project_id}/(bundle.zip|template.aep); render claim probes and
returns is_bundle+md5; node-agent extracts the bundle, locates the .aep
(zip-slip guarded), and caches by md5 so repeated renders extract once.
AE template scanner ("read scenes/colours/configs from the AEP"):
- content-svc importer: POST /v1/projects/{id}/scan/{preview,apply} —
review-diff-then-merge into scenes/elements/colours (manual edits kept).
- render-svc Go quick-scan: stdlib RIFX parser extracts comp names+durations
(no AE) → POST /v1/template-scans/{id}/quick.
- render-svc AE scan jobs + node-agent runner: queue → node runs scan.jsx
(reverse of legacy JSXGenerator conventions: frfinal/frshare/frl_/frd_) →
posts ScanResult back. Migration 26_render_scan_jobs.
- admin UI: "اسکن از افترافکت" with quick/full engines + diff-review modal.
Verified: importer preview/apply, Go quick-scan end-to-end (synthetic .aep →
scene imported), bundle extract unit tests, RIFX parser unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
@@ -46,6 +46,7 @@ type Agent struct {
|
||||
orch *client.Client
|
||||
mu sync.Mutex
|
||||
currentJob *client.ClaimedJob
|
||||
scanning bool // true while running an AE scan job (shares the AE app)
|
||||
status string // "Ready" | "Busy"
|
||||
}
|
||||
|
||||
@@ -75,9 +76,25 @@ func (a *Agent) getStatus() (string, *string) {
|
||||
jobID := a.currentJob.JobID
|
||||
return a.status, &jobID
|
||||
}
|
||||
if a.scanning {
|
||||
return "Busy", nil
|
||||
}
|
||||
return a.status, nil
|
||||
}
|
||||
|
||||
func (a *Agent) setScanning(v bool) {
|
||||
a.mu.Lock()
|
||||
a.scanning = v
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// isBusy reports whether the AE app is in use (rendering or scanning).
|
||||
func (a *Agent) isBusy() bool {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.currentJob != nil || a.scanning
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
@@ -121,10 +138,11 @@ func main() {
|
||||
|
||||
// Main loops
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
wg.Add(4)
|
||||
go func() { defer wg.Done(); agent.heartbeatLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.pollLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.fontSyncLoop(ctx) }()
|
||||
go func() { defer wg.Done(); agent.scanLoop(ctx) }()
|
||||
wg.Wait()
|
||||
log.Printf("shutdown complete")
|
||||
}
|
||||
@@ -183,6 +201,83 @@ func (a *Agent) syncFonts(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scan loop ──────────────────────────────────────────────────────────────────
|
||||
// Claims AE scan jobs and runs the template scanner (scan.jsx) via afterfx.exe,
|
||||
// posting the resulting ScanResult JSON back to the orchestrator. Requires the AE
|
||||
// app — skipped entirely in mock/dev (no AE_PATH).
|
||||
|
||||
func (a *Agent) scanLoop(ctx context.Context) {
|
||||
interval := time.Duration(a.cfg.PollIntervalSec) * time.Second
|
||||
if interval < 5*time.Second {
|
||||
interval = 5 * time.Second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
a.pollScanOnce(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) pollScanOnce(ctx context.Context) {
|
||||
if a.cfg.AfterFxPath == "" || a.cfg.AEPath == "" {
|
||||
return // scanning needs the real AE app
|
||||
}
|
||||
if a.isBusy() {
|
||||
return // don't contend with a render (or another scan) for the AE app
|
||||
}
|
||||
|
||||
claim, err := a.orch.ClaimScan(ctx, a.cfg.NodeID, a.cfg.Region)
|
||||
if err != nil {
|
||||
log.Printf("scan claim error: %v", err)
|
||||
return
|
||||
}
|
||||
if claim == nil {
|
||||
return // nothing queued
|
||||
}
|
||||
|
||||
a.setScanning(true)
|
||||
defer a.setScanning(false)
|
||||
log.Printf("[scan %s] claimed (project %s)", claim.ScanJobID, claim.ProjectID)
|
||||
|
||||
// Reuse the template prepare/cache pipeline (download + extract bundle by md5).
|
||||
prepCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
|
||||
aepPath, perr := runner.PrepareTemplate(prepCtx, claim.AEPDownloadURL, claim.IsBundle, claim.BundleMD5, a.cfg.WorkDir, "scan-"+claim.ScanJobID)
|
||||
cancel()
|
||||
if perr != nil {
|
||||
a.failScan(claim.ScanJobID, "prepare template: "+perr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
outPath := filepath.Join(a.cfg.WorkDir, "scans", claim.ScanJobID, "scan.json")
|
||||
scanCtx, cancel2 := context.WithTimeout(ctx, 10*time.Minute)
|
||||
result, serr := runner.RunScan(scanCtx, a.cfg.AfterFxPath, aepPath, a.cfg.WorkDir, outPath)
|
||||
cancel2()
|
||||
if serr != nil {
|
||||
a.failScan(claim.ScanJobID, serr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rptCtx, cancel3 := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel3()
|
||||
if err := a.orch.ReportScanResult(rptCtx, claim.ScanJobID, result); err != nil {
|
||||
log.Printf("[scan %s] report result error: %v", claim.ScanJobID, err)
|
||||
return
|
||||
}
|
||||
log.Printf("[scan %s] done (%d bytes)", claim.ScanJobID, len(result))
|
||||
}
|
||||
|
||||
func (a *Agent) failScan(id, reason string) {
|
||||
log.Printf("[scan %s] failed: %s", id, reason)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_ = a.orch.ReportScanFail(ctx, id, reason)
|
||||
}
|
||||
|
||||
// ── Heartbeat loop ────────────────────────────────────────────────────────────
|
||||
|
||||
func (a *Agent) heartbeatLoop(ctx context.Context) {
|
||||
@@ -267,18 +362,24 @@ func (a *Agent) tryClaimAndRun(ctx context.Context) {
|
||||
func (a *Agent) runJob(ctx context.Context, job *client.ClaimedJob) {
|
||||
log.Printf("[job %s] starting render", job.JobID)
|
||||
|
||||
// ── Step 1: Download .aep template ───────────────────────────────────────
|
||||
// ── Step 1: Fetch + prepare the .aep template ────────────────────────────
|
||||
// PrepareTemplate downloads the project file; if it's a .zip bundle it is
|
||||
// extracted and the .aep inside located. Prepared templates are cached by md5
|
||||
// so repeated renders of the same template skip the download + extraction.
|
||||
aepPath := ""
|
||||
if job.AEPDownloadURL != "" && a.cfg.AEPath != "" {
|
||||
localAEP := filepath.Join(a.cfg.WorkDir, "templates", job.JobID, "template.aep")
|
||||
dlCtx, dlCancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
n, dlErr := runner.DownloadFile(dlCtx, job.AEPDownloadURL, localAEP)
|
||||
dlCtx, dlCancel := context.WithTimeout(ctx, 15*time.Minute)
|
||||
p, prepErr := runner.PrepareTemplate(dlCtx, job.AEPDownloadURL, job.IsBundle, job.BundleMD5, a.cfg.WorkDir, job.JobID)
|
||||
dlCancel()
|
||||
if dlErr != nil {
|
||||
log.Printf("[job %s] AEP download failed (%v) — falling back to mock", job.JobID, dlErr)
|
||||
if prepErr != nil {
|
||||
log.Printf("[job %s] template prepare failed (%v) — falling back to mock", job.JobID, prepErr)
|
||||
} else {
|
||||
log.Printf("[job %s] AEP downloaded (%d bytes) → %s", job.JobID, n, localAEP)
|
||||
aepPath = localAEP
|
||||
kind := "aep"
|
||||
if job.IsBundle {
|
||||
kind = "bundle"
|
||||
}
|
||||
log.Printf("[job %s] template ready (%s) → %s", job.JobID, kind, p)
|
||||
aepPath = p
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user