package agent import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/gogf/gf/v2/frame/g" ) const ( renderFramesCount = 36 renderFrameSize = "256x512" renderTimeout = 15 * time.Minute ) // RenderAvatarFrames 将化身 GLB 预渲染为绕 Y 轴旋转帧序列,返回帧目录访问 URL。 // 帧目录已就绪直接复用;render.enabled=false 或渲染失败时返回 error,由调用方降级。 func RenderAvatarFrames(ctx context.Context, glb string, outKey string) (framesURL string, err error) { if !g.Cfg().MustGet(ctx, "render.enabled", true).Bool() { return "", errors.New("3D 渲染服务未启用") } dir := filepath.Join("workspace", "avatar_frames", outKey) if framesReady(dir) { return "/workspace/avatar_frames/" + outKey, nil } if _, err := os.Stat(glb); err != nil { return "", fmt.Errorf("化身 GLB 不存在: %w", err) } if err := runRender(ctx, glb, dir); err != nil { return "", err } return "/workspace/avatar_frames/" + outKey, nil } func framesReady(dir string) bool { entries, err := os.ReadDir(dir) if err != nil { return false } count := 0 for _, e := range entries { if strings.HasPrefix(e.Name(), "frame_") && strings.HasSuffix(e.Name(), ".png") { count++ } } return count >= renderFramesCount } func nodeBin(ctx context.Context) string { bin := g.Cfg().MustGet(ctx, "render.node_bin", "node").String() if bin == "" { return "node" } // 配置路径不存在(如容器环境)→ 回退 PATH 中的 node if _, err := os.Stat(bin); err != nil { return "node" } return bin } func runRender(ctx context.Context, glb, out string) error { if err := os.MkdirAll(out, 0o755); err != nil { return err } return execNode(ctx, filepath.Join("scripts", "avatar-render", "render.js"), "--glb", glb, "--out", out, "--frames", fmt.Sprint(renderFramesCount), "--size", renderFrameSize) } func execNode(ctx context.Context, script string, args ...string) error { cmdCtx, cancel := context.WithTimeout(ctx, renderTimeout) defer cancel() cmd := exec.CommandContext(cmdCtx, nodeBin(ctx), append([]string{script}, args...)...) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("node 渲染失败: %v: %s", err, strings.TrimSpace(string(out))) } return nil }