You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Autodesk Forge API将转换后的SVF文件上传至OSS存储桶?

解决Autodesk Forge SVF文件上传至OSS存储桶的问题

核心问题分析

你当前代码里的GetDerivativeManifestAsync方法返回的是衍生转换的清单数据(JSON格式),并非SVF文件的实际字节内容,这是导致上传错误的关键。要获取SVF文件,需要先通过清单定位到SVF文件的下载URL,再下载文件内容,最后上传到OSS。

正确实现步骤

  1. 等待转换完成:调用TranslateAsync后,转换是异步执行的,不能立即获取文件,需通过轮询或Webhook确认转换完成。
  2. 获取衍生文件清单:调用GetDerivativeManifestAsync获取所有衍生文件的元信息,从中定位SVF文件的标识。
  3. 获取SVF下载URL:通过GetDerivativeUrlAsync获取SVF文件的临时下载链接。
  4. 下载SVF文件:用HTTP客户端下载SVF文件的字节流。
  5. 上传至OSS:使用Forge的ObjectsApi将字节流上传到指定OSS存储桶。

修正后的完整代码示例

[HttpPost]
[Route("/api/aps/callback/designautomation")]
public async Task<IActionResult> OnCallback(string id, string outputFileName, string rootFile, [FromBody] dynamic body)
{
    Console.WriteLine("OnCallback 1");
    try
    {
        JObject bodyJson = JObject.Parse((string)body.ToString());
        await _hubContext.Clients.Client(id).SendAsync("onComplete", bodyJson.ToString());

        using (var httpClient = new HttpClient())
        {
            byte[] bs = await httpClient.GetByteArrayAsync(bodyJson["reportUrl"]?.Value<string>());
            string report = System.Text.Encoding.Default.GetString(bs);
            await _hubContext.Clients.Client(id).SendAsync("onComplete", report);
        }

        // 获取OAuth令牌
        dynamic oauth = await OAuthController.GetInternalAsync();

        ObjectsApi objectsApi = new ObjectsApi();
        objectsApi.Configuration.AccessToken = oauth.access_token;

        // 获取Design Automation输出文件的下载URL
        ApiResponse<dynamic> res = await objectsApi.getS3DownloadURLAsyncWithHttpInfo(
                                    NickName.ToLower() + "-design",
                                    outputFileName, new Dictionary<string, object> {
                                        { "minutesExpiration", 15.0 },
                                        { "useCdn", true }
                                    });
        await _hubContext.Clients.Client(id).SendAsync("downloadResult", (string)(res.Data.url));
        Console.WriteLine("Congrats!");

        // 获取输出文件详情,用于后续转换
        res = await objectsApi.GetObjectDetailsAsyncWithHttpInfo(
            NickName.ToLower() + "-design",
            outputFileName);

        DerivativesApi derivativesApi = new DerivativesApi();
        derivativesApi.Configuration.AccessToken = oauth.access_token;
        var formats = new List<JobPayloadItem> {
            new JobPayloadItem (JobPayloadItem.TypeEnum.Svf, new List<JobPayloadItem.ViewsEnum> {
                JobPayloadItem.ViewsEnum._2d, JobPayloadItem.ViewsEnum._3d })
        };
        var urn = Base64Encode(res.Data.objectId);
        var payload = new JobPayload(
            new JobPayloadInput(urn, true, rootFile),
            new JobPayloadOutput(formats)
        );

        // 提交转换任务
        res = await derivativesApi.TranslateAsyncWithHttpInfo(payload);
        await _hubContext.Clients.Client(id).SendAsync("urn", (string)urn);

        // 轮询等待转换完成(生产环境建议用Webhook替代)
        bool isCompleted = false;
        int retryCount = 0;
        const int maxRetries = 20;
        const int delaySeconds = 10;
        dynamic manifest = null;

        while (!isCompleted && retryCount < maxRetries)
        {
            try
            {
                manifest = await derivativesApi.GetDerivativeManifestAsync(oauth.access_token, urn);
                string status = manifest.status;
                if (status.Equals("success", StringComparison.OrdinalIgnoreCase))
                {
                    isCompleted = true;
                }
                else if (status.Equals("failed", StringComparison.OrdinalIgnoreCase))
                {
                    throw new Exception("文件转换失败");
                }
                else
                {
                    await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
                    retryCount++;
                }
            }
            catch (ApiException ex) when (ex.StatusCode == 404)
            {
                // 清单尚未生成,继续等待
                await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
                retryCount++;
            }
        }

        if (!isCompleted)
        {
            throw new Exception("文件转换超时");
        }

        // 从清单中定位SVF文件的GUID
        string svfGuid = null;
        if (manifest.derivatives != null && manifest.derivatives.Count > 0)
        {
            var svfDerivative = manifest.derivatives.FirstOrDefault(d => d.outputType.Equals("svf", StringComparison.OrdinalIgnoreCase));
            if (svfDerivative != null && svfDerivative.children != null)
            {
                var svfFile = svfDerivative.children.FirstOrDefault(c => c.role.Equals("3d", StringComparison.OrdinalIgnoreCase) && c.type.Equals("svf", StringComparison.OrdinalIgnoreCase));
                svfGuid = svfFile?.guid;
            }
        }

        if (string.IsNullOrEmpty(svfGuid))
        {
            throw new Exception("未找到SVF文件的标识");
        }

        // 获取SVF文件的临时下载URL
        string svfDownloadUrl = await derivativesApi.GetDerivativeUrlAsync(oauth.access_token, urn, svfGuid);

        // 下载SVF文件字节流
        byte[] svfFileBytes;
        using (var httpClient = new HttpClient())
        {
            svfFileBytes = await httpClient.GetByteArrayAsync(svfDownloadUrl);
        }

        // 上传至OSS存储桶
        string ossBucketName = NickName.ToLower() + "-design";
        string translatedFileName = "model.svf"; // 自定义目标文件名
        using (var memoryStream = new MemoryStream(svfFileBytes))
        {
            await objectsApi.UploadObjectAsync(
                ossBucketName,
                translatedFileName,
                svfFileBytes.Length,
                memoryStream,
                new Dictionary<string, string>(),
                null,
                "application/octet-stream"
            );
        }

        await _hubContext.Clients.Client(id).SendAsync("svfUploaded", "SVF文件已成功上传至OSS");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        await _hubContext.Clients.Client(id).SendAsync("error", ex.Message);
    }

    return Ok();
}

// 辅助方法:URL安全的Base64编码
private string Base64Encode(string input)
{
    var bytes = System.Text.Encoding.UTF8.GetBytes(input);
    return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');
}

最佳实践建议

  • 用Webhook替代轮询:生产环境不要用轮询等待转换完成,提交转换任务时指定Webhook URL,Forge会在转换完成后主动推送通知,提升效率。
  • 大文件分块上传:如果SVF文件超过100MB,使用UploadObjectChunkAsync进行分块上传,避免内存溢出。
  • 添加重试机制:对HTTP请求、API调用增加重试逻辑,处理网络波动或临时错误。
  • 权限校验:确保OSS存储桶权限配置正确,OAuth令牌需拥有data:write和bucket:write权限。

内容的提问来源于stack exchange,提问作者piotkro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 18:42:01