ソースを参照

接入百度的seedance2.0demo

lh 1 ヶ月 前
コミット
9bac15a8a1

+ 1 - 0
app/Models/MpAsset.php

@@ -19,6 +19,7 @@ class MpAsset extends Model
         'url',
         'cover_url',
         'asset_type',
+        'source_type',
         'status',
         'error_message',
         'extra_params',

+ 5 - 4
app/Services/AIGeneration/AIImageGenerationService.php

@@ -1151,8 +1151,8 @@ class AIImageGenerationService
                 ];
             }
 
-            // 先获取表中是否已有该url
-            $asset = DB::table('mp_assets')->where('url', $params['url'])->first();
+            // 先获取表中是否已有该url(仅限智真AI来源的素材)
+            $asset = DB::table('mp_assets')->where('url', $params['url'])->where('source_type', 'zhizhen')->first();
             if ($asset) {
                 return [
                     'code' => 0,
@@ -1224,6 +1224,7 @@ class AIImageGenerationService
                 'name' => $params['name'] ?? null,
                 'url' => $params['url'],
                 'asset_type' => $params['asset_type'] ?? MpAsset::TYPE_IMAGE,
+                'source_type' => 'zhizhen',
                 'status' => MpAsset::STATUS_PROCESSING,
                 'extra_params' => $responseData,
             ]);
@@ -1269,8 +1270,8 @@ class AIImageGenerationService
                 ];
             }
 
-            // 先获取表中是否已有该url
-            $assetInfo = DB::table('mp_assets')->where('asset_id', $assetId)->first();
+            // 先获取表中是否已有该素材(仅限智真AI来源的素材)
+            $assetInfo = DB::table('mp_assets')->where('asset_id', $assetId)->where('source_type', 'zhizhen')->first();
             if ($assetInfo) {
                 $assetInfo = (array)$assetInfo;
                 $status = getProp($assetInfo, 'status');

+ 507 - 17
app/Services/AIGeneration/AIVideoGenerationService.php

@@ -3,6 +3,7 @@
 namespace App\Services\AIGeneration;
 
 use App\Libs\Utils;
+use App\Models\MpAsset;
 use App\Models\MpGenerateVideoTask;
 use App\Models\AIElement;
 use App\Services\VolcEngineService;
@@ -480,6 +481,57 @@ class AIVideoGenerationService
     }
 
     /**
+     * 创建 Seedance 2.0(百度AI网关)视频生成任务
+     *
+     * @param array $params
+     * @return MpGenerateVideoTask
+     */
+    public function createSeedance20Task(array $params): MpGenerateVideoTask
+    {
+        // 生成唯一的任务ID
+        $taskId = 'seedance20_' . time() . '_' . uniqid();
+
+        // 创建任务记录,复用现有表结构
+        $task = MpGenerateVideoTask::create([
+            'task_id' => $taskId,
+            'alias_segment_id' => $params['alias_segment_id'] ?? '',
+            'alias_act_id' => $params['alias_act_id'] ?? '',
+            'prompt' => $params['prompt'] ?? '',
+            'first_frame_url' => $params['first_frame_url'] ?? '',
+            'tail_frame_url' => $params['tail_frame_url'] ?? '',
+            'seed' => $params['seed'] ?? -1,
+            'frames' => $params['video_duration'] ? ($params['video_duration'] * 24 + 1) : null,
+            'video_resolution' => $params['video_resolution'] ?? '720P',
+            'status' => MpGenerateVideoTask::STATUS_PENDING,
+            'api_type' => 'seedance', // 复用Seedance任务轮询流程(提交/查询接口按extra_params区分网关)
+            'extra_params' => [
+                'model' => $params['model'] ?? 'doubao-seedance-2.0',
+                'content' => $params['content'],
+                'reference_images' => $params['reference_images'] ?? [],
+                'reference_image_assets' => $params['reference_image_assets'] ?? [],
+                'callback_url' => $params['callback_url'] ?? '',
+                'return_last_frame' => $params['return_last_frame'] ?? true,
+                'execution_expires_after' => $params['execution_expires_after'] ?? 172800,
+                'video_resolution' => $params['video_resolution'] ?? '720P',
+                'generate_audio' => $params['generate_audio'] ?? false,
+                'draft' => $params['draft'] ?? false,
+                'ratio' => $params['ratio'] ?? '9:16',
+                'duration' => $params['video_duration'] ?? 5,
+                'camera_fixed' => $params['camera_fixed'] ?? false,
+                'watermark' => $params['watermark'] ?? false,
+                // 百度AI网关配置
+                'gateway_base_url' => 'https://ai-gateway.baidubce.com',
+                'api_key_env' => 'BAIDU_SEEDANCE_20_AK',
+            ]
+        ]);
+
+        // 提交任务到百度AI网关
+        $this->submitSeedanceTaskToApi($task);
+
+        return $task;
+    }
+
+    /**
      * 提交 Seedance 任务到火山引擎 API
      *
      * @param MpGenerateVideoTask $task
@@ -489,22 +541,49 @@ class AIVideoGenerationService
     {
         $client = new Client(['verify'=>false,'timeout'=>120]);
         try {
+            $extraParams = $task->extra_params;
+
+            // 网关配置:Seedance 2.0 走百度AI网关,其余默认走火山引擎
+            $gatewayBaseUrl = $extraParams['gateway_base_url'] ?? 'https://ark.cn-beijing.volces.com';
+            $apiKeyEnv = $extraParams['api_key_env'] ?? 'VOLC_AI_API_KEY';
+
             // 验证环境变量配置
-            $apiKey = env('VOLC_AI_API_KEY');
-                
+            $apiKey = env($apiKeyEnv);
+
             if (empty($apiKey)) {
                 $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
-                    'error_message' => '火山引擎访问密钥未配置,请检查环境变量API Key'
+                    'error_message' => $apiKeyEnv . ' 未配置,请检查环境变量API Key'
                 ]);
                 return;
             }
 
-            $extraParams = $task->extra_params;
+            $content = $extraParams['content'] ?? [];
+
+            // Seedance 2.0(百度AI网关)需要先把图片上传为素材
+            if (!empty($extraParams['gateway_base_url'])) {
+                $content = $this->convertSeedance20ContentImagesToAssets($content);
+            }
+
+            // 追加参考图素材(已由processReferenceImagesToSeedance20Assets上传并清理失败的图片标记)
+            if (!empty($extraParams['gateway_base_url']) && !empty($extraParams['reference_image_assets'])) {
+                foreach ($extraParams['reference_image_assets'] as $assetUrl) {
+                    if (empty($assetUrl)) {
+                        continue;
+                    }
+                    $content[] = [
+                        'type' => 'image_url',
+                        'image_url' => [
+                            'url' => $assetUrl,
+                        ],
+                        'role' => 'reference_image',
+                    ];
+                }
+            }
             
             // 构建火山引擎 Seedance API 请求参数
             $apiParams = [
                 'model' => isset(\App\Consts\BaseConst::MODELS_MAP[$extraParams['model']]) ? \App\Consts\BaseConst::MODELS_MAP[$extraParams['model']] : $extraParams['model'],
-                'content' => $extraParams['content'],
+                'content' => $content,
             ];
 
             // 添加可选参数
@@ -514,7 +593,7 @@ class AIVideoGenerationService
             if (isset($extraParams['return_last_frame'])) {
                 $apiParams['return_last_frame'] = $extraParams['return_last_frame'];
             }
-            if (!empty($extraParams['service_tier']) && !in_array($extraParams['model'], ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128'])) {
+            if (!empty($extraParams['service_tier']) && !in_array($extraParams['model'], ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'doubao-seedance-2.0'])) {
                 $apiParams['service_tier'] = $extraParams['service_tier'];
             }
             if (!empty($extraParams['execution_expires_after'])) {
@@ -548,11 +627,11 @@ class AIVideoGenerationService
                 $apiParams['watermark'] = $extraParams['watermark'];
             }
             
-            dLog('generate')->info('Seedance 1.5 Pro 视频生成参数: ', $apiParams);
-            logDB('generate', 'info', 'Seedance视频生成任务提交', ['id' => $task->id, 'params' => $apiParams]);
+            dLog('generate')->info('Seedance 视频生成参数: ', $apiParams);
+            logDB('generate', 'info', 'Seedance视频生成任务提交', ['id' => $task->id, 'model' => $apiParams['model'], 'params' => $apiParams]);
             
-            // 调用火山引擎 Seedance API
-            $response = $client->post('https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks', [
+            // 调用 Seedance API
+            $response = $client->post($gatewayBaseUrl . '/api/v3/contents/generations/tasks', [
                 'headers' => [
                     'Authorization' => 'Bearer ' . $apiKey,
                     'Content-Type' => 'application/json',
@@ -562,7 +641,7 @@ class AIVideoGenerationService
 
 
             $responseData = json_decode($response->getBody(), true);
-            dLog('generate')->info('Seedance 1.5 Pro API 响应: ', $responseData);
+            dLog('generate')->info('Seedance API 响应: ', $responseData);
             logDB('generate', 'info', 'Seedance视频生成API响应', ['id' => $task->id, 'response' => $responseData]);
                 
             if (isset($responseData['error'])) {
@@ -594,6 +673,413 @@ class AIVideoGenerationService
         }
     }
 
+    /**
+     * 将 Seedance 2.0 参考图上传为百度AI网关素材
+     * 失败处理与智真AI的processReferenceImagesToAssets一致:删除参考图的同时清理提示词中的<图片N>标记
+     *
+     * @param array $referenceImages 参考图片URL数组
+     * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败图片的标记)
+     * @return array 返回素材ID数组(asset://格式)
+     */
+    public function processReferenceImagesToSeedance20Assets(array $referenceImages, string &$fullPrompt): array
+    {
+        $assetIds = [];
+        $failedIndexes = []; // 记录失败的图片序号
+
+        foreach ($referenceImages as $index => $imageUrl) {
+            $imageIndex = $index + 1; // 图片序号从1开始
+
+            try {
+                dLog('generate')->info("开始处理Seedance 2.0参考图片 {$imageIndex}: {$imageUrl}");
+
+                // 上传素材(已存在则直接复用,不重复上传)
+                $createResult = $this->createSeedance20Asset($imageUrl, "参考图片_{$imageIndex}_" . time());
+
+                if ($createResult['code'] !== 0) {
+                    dLog('generate')->warning("图片 {$imageIndex} 创建Seedance 2.0素材失败: " . $createResult['msg']);
+                    logDB('generate', 'warning', 'Seedance2.0创建素材失败', [
+                        'image_url' => $imageUrl,
+                        'index' => $imageIndex,
+                        'error' => $createResult['msg']
+                    ]);
+                    $failedIndexes[] = $imageIndex;
+                    continue;
+                }
+
+                $assetIds[] = $createResult['data']['asset_id'];
+                dLog('generate')->info("图片 {$imageIndex} Seedance 2.0素材上传成功,asset_id: " . $createResult['data']['asset_id']);
+
+            } catch (\Exception $e) {
+                dLog('generate')->error("图片 {$imageIndex} Seedance 2.0素材处理异常: " . $e->getMessage());
+                logDB('generate', 'error', 'Seedance2.0素材处理异常', [
+                    'image_url' => $imageUrl,
+                    'index' => $imageIndex,
+                    'error' => $e->getMessage()
+                ]);
+                $failedIndexes[] = $imageIndex;
+            }
+        }
+
+        // 处理失败的图片:从 fullPrompt 中删除对应的标记并调整序号
+        if (!empty($failedIndexes)) {
+            dLog('generate')->info('开始处理Seedance 2.0失败图片的提示词调整,失败序号: ' . implode(',', $failedIndexes));
+
+            // 按序号从大到小排序,避免删除时影响后续序号
+            rsort($failedIndexes);
+
+            foreach ($failedIndexes as $failedIndex) {
+                // 删除失败图片的标记,例如 <图片1>、<图片2> 等
+                $pattern = "/<图片{$failedIndex}>/u";
+                $fullPrompt = preg_replace($pattern, '', $fullPrompt);
+                dLog('generate')->info("已从提示词中删除 <图片{$failedIndex}>");
+            }
+
+            // 重新调整剩余图片的序号
+            $needAdjust = false;
+            foreach ($failedIndexes as $failedIndex) {
+                $needAdjust = true;
+                // 将所有大于失败序号的图片标记序号减1
+                for ($i = $failedIndex + 1; $i <= count($referenceImages); $i++) {
+                    $oldPattern = "/<图片{$i}>/u";
+                    $newTag = "<图片" . ($i - 1) . ">";
+                    $fullPrompt = preg_replace($oldPattern, $newTag, $fullPrompt);
+                }
+            }
+
+            if ($needAdjust) {
+                dLog('generate')->info('Seedance 2.0提示词图片序号调整完成');
+                logDB('generate', 'info', 'Seedance2.0提示词调整', [
+                    'failed_indexes' => $failedIndexes,
+                    'adjusted_prompt' => $fullPrompt
+                ]);
+            }
+        }
+
+        dLog('generate')->info('Seedance 2.0参考图片转素材完成,成功数量: ' . count($assetIds) . ',失败数量: ' . count($failedIndexes));
+
+        // 格式化为可直接在API接口中使用的数组
+        foreach ($assetIds as &$assetId) {
+            $assetId = "asset://{$assetId}";
+        }
+
+        return $assetIds;
+    }
+
+    /**
+     * 将 Seedance 2.0 内容中的图片URL上传为百度AI网关素材
+     * 上传失败的图片直接从content中移除,不阻断整个任务
+     *
+     * @param array $content
+     * @return array 转换后的content(图片URL替换为 asset://{AssetId})
+     */
+    private function convertSeedance20ContentImagesToAssets(array $content): array
+    {
+        foreach ($content as $index => &$item) {
+            if (($item['type'] ?? '') !== 'image_url') {
+                continue;
+            }
+
+            $imageUrl = $item['image_url']['url'] ?? '';
+            if (empty($imageUrl) || strpos($imageUrl, 'asset://') === 0) {
+                continue;
+            }
+
+            $name = 'seedance20_' . time() . '_' . uniqid();
+            $createResult = $this->createSeedance20Asset($imageUrl, $name);
+
+            if ($createResult['code'] !== 0) {
+                // 上传失败:移除该图片,继续处理其他图片
+                dLog('generate')->warning('Seedance 2.0 图片上传素材失败,已从content中移除: ' . $imageUrl . ',原因: ' . $createResult['msg']);
+                logDB('generate', 'warning', 'Seedance2.0图片转素材失败', [
+                    'image_url' => $imageUrl,
+                    'error' => $createResult['msg']
+                ]);
+                unset($content[$index]);
+                continue;
+            }
+
+            $item['image_url']['url'] = 'asset://' . $createResult['data']['asset_id'];
+            dLog('generate')->info('Seedance 2.0 图片转素材成功: ' . $imageUrl . ' => asset://' . $createResult['data']['asset_id']);
+        }
+        unset($item);
+
+        return array_values($content);
+    }
+
+    /**
+     * 上传图片到百度AI网关素材库并等待素材可用
+     * 上传前先检查mp_assets中是否已存在该url(source_type=baidu),存在则直接复用素材ID,避免重复上传
+     *
+     * @param string $url 图片公网地址
+     * @param string $name 素材名称
+     * @return array ['code'=>0, 'data'=>['asset_id'=>...]] 成功;['code'=>20003, 'msg'=>...] 失败
+     */
+    private function createSeedance20Asset(string $url, string $name): array
+    {
+        try {
+            if (empty($url)) {
+                return [
+                    'code' => 1,
+                    'msg' => '素材URL不能为空',
+                    'data' => null
+                ];
+            }
+
+            // 先获取表中是否已有该url(仅限百度网关来源的素材)
+            $asset = DB::table('mp_assets')->where('url', $url)->where('source_type', 'baidu')->first();
+            if ($asset) {
+                dLog('generate')->info('Seedance 2.0 素材已存在,直接复用: ' . $url . ' => ' . $asset->asset_id);
+                return [
+                    'code' => 0,
+                    'msg' => '创建成功',
+                    'data' => [
+                        'asset_id' => $asset->asset_id,
+                        'group_id' => $asset->group_id,
+                    ]
+                ];
+            }
+
+            $groupId = $this->ensureSeedance20AssetGroup();
+
+            $responseData = $this->requestSeedance20Gateway('CreateAsset', [
+                'ProjectName' => 'default',
+                'GroupId' => $groupId,
+                'AssetType' => 'Image',
+                'Name' => $name,
+                'URL' => $url,
+            ]);
+
+            $result = $responseData['Result'] ?? [];
+            $assetId = $result['Id'] ?? $result['AssetId'] ?? '';
+            if (empty($assetId)) {
+                logDB('generate', 'error', 'Seedance2.0创建素材失败', ['url' => $url, 'response' => $responseData]);
+                return [
+                    'code' => 20003,
+                    'msg' => '创建Seedance 2.0素材失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE),
+                    'data' => null
+                ];
+            }
+
+            // 保存素材记录到数据库
+            MpAsset::create([
+                'asset_id' => $assetId,
+                'group_id' => $groupId,
+                'group_name' => 'default',
+                'name' => $name,
+                'url' => $url,
+                'asset_type' => MpAsset::TYPE_IMAGE,
+                'source_type' => 'baidu',
+                'status' => MpAsset::STATUS_PROCESSING,
+                'extra_params' => $responseData,
+            ]);
+
+            // 轮询查询素材状态(每3秒一次,最多100次即5分钟)
+            $maxAttempts = 100;
+            $assetStatus = null;
+            for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
+                sleep(3);
+
+                $assetInfo = $this->getSeedance20Asset($assetId);
+                $assetStatus = $assetInfo['Status'] ?? '';
+                dLog('generate')->info("Seedance 2.0 素材 {$assetId} 状态查询第 {$attempt} 次,状态: {$assetStatus}");
+
+                if ($assetStatus === 'Active') {
+                    logDB('generate', 'info', 'Seedance2.0素材处理成功', [
+                        'asset_id' => $assetId,
+                        'url' => $url
+                    ]);
+                    return [
+                        'code' => 0,
+                        'msg' => '创建成功',
+                        'data' => [
+                            'asset_id' => $assetId,
+                            'group_id' => $groupId,
+                        ]
+                    ];
+                }
+
+                if ($assetStatus === 'Failed') {
+                    $errorMessage = $assetInfo['Error']['Message'] ?? json_encode($assetInfo, JSON_UNESCAPED_UNICODE);
+                    logDB('generate', 'error', 'Seedance2.0素材处理失败', [
+                        'asset_id' => $assetId,
+                        'url' => $url,
+                        'error' => $errorMessage
+                    ]);
+                    return [
+                        'code' => 20003,
+                        'msg' => 'Seedance 2.0素材处理失败: ' . $errorMessage,
+                        'data' => null
+                    ];
+                }
+            }
+
+            logDB('generate', 'error', 'Seedance2.0素材处理超时', [
+                'asset_id' => $assetId,
+                'url' => $url,
+                'last_status' => $assetStatus
+            ]);
+            return [
+                'code' => 20003,
+                'msg' => 'Seedance 2.0素材处理超时: ' . $assetId . ',最后状态: ' . ($assetStatus ?? 'unknown'),
+                'data' => null
+            ];
+        } catch (\Exception $e) {
+            dLog('generate')->error('Seedance 2.0创建素材异常: ' . $e->getMessage());
+            logDB('generate', 'error', 'Seedance2.0创建素材异常', ['url' => $url, 'error' => $e->getMessage()]);
+            return [
+                'code' => 20003,
+                'msg' => '创建Seedance 2.0素材失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 查询百度AI网关素材状态(优先查本地mp_assets,处理中/缺失时请求网关并同步状态)
+     *
+     * @param string $assetId
+     * @return array 素材信息(含Status字段)
+     */
+    private function getSeedance20Asset(string $assetId): array
+    {
+        // 先查本地素材表(仅限百度网关来源的素材)
+        $assetInfo = DB::table('mp_assets')->where('asset_id', $assetId)->where('source_type', 'baidu')->first();
+        if ($assetInfo) {
+            $assetInfo = (array)$assetInfo;
+            $status = getProp($assetInfo, 'status');
+            if (in_array($status, [MpAsset::STATUS_ACTIVE, MpAsset::STATUS_FAILED])) {
+                $result = [
+                    'Id' => $assetInfo['asset_id'],
+                    'Status' => $status,
+                    'URL' => $assetInfo['url'] ?? '',
+                ];
+                if ($status === MpAsset::STATUS_FAILED && !empty($assetInfo['error_message'])) {
+                    $result['Error'] = ['Message' => $assetInfo['error_message']];
+                }
+                return $result;
+            }
+        }
+
+        $responseData = $this->requestSeedance20Gateway('GetAsset', [
+            'ProjectName' => 'default',
+            'Id' => $assetId,
+        ]);
+
+        $result = $responseData['Result'] ?? [];
+        // 兼容不同返回结构
+        if (isset($result['Asset']) && is_array($result['Asset'])) {
+            $result = $result['Asset'];
+        }
+
+        // 查询失败时快速结束,避免长时间轮询
+        if (empty($result) && isset($responseData['Error'])) {
+            $result['Status'] = 'Failed';
+            $result['Error'] = $responseData['Error'];
+        }
+
+        // 同步更新本地素材状态
+        if (!empty($result['Status'])) {
+            $updateData = ['status' => $result['Status']];
+            if (!empty($result['Error']['Message'])) {
+                $updateData['error_message'] = $result['Error']['Message'];
+            }
+            DB::table('mp_assets')
+                ->where('asset_id', $assetId)
+                ->where('source_type', 'baidu')
+                ->update($updateData);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取可用的Seedance 2.0素材组ID(不存在则创建默认素材组)
+     *
+     * @return string
+     */
+    private function ensureSeedance20AssetGroup(): string
+    {
+        $groups = $this->listSeedance20AssetGroups();
+        if (!empty($groups)) {
+            $groupId = $groups[0]['Id'] ?? $groups[0]['GroupId'] ?? '';
+            if (!empty($groupId)) {
+                return $groupId;
+            }
+        }
+
+        return $this->createSeedance20AssetGroup();
+    }
+
+    /**
+     * 查询百度AI网关素材组列表
+     *
+     * @return array
+     */
+    private function listSeedance20AssetGroups(): array
+    {
+        $responseData = $this->requestSeedance20Gateway('ListAssetGroups', [
+            'ProjectName' => 'default',
+        ]);
+
+        $result = $responseData['Result'] ?? [];
+        $groups = $result['AssetGroups'] ?? $result['Groups'] ?? $result['Items'] ?? [];
+
+        return is_array($groups) ? $groups : [];
+    }
+
+    /**
+     * 创建Seedance 2.0默认素材组
+     *
+     * @return string 素材组ID
+     */
+    private function createSeedance20AssetGroup(): string
+    {
+        $responseData = $this->requestSeedance20Gateway('CreateAssetGroup', [
+            'ProjectName' => 'default',
+            'Name' => 'default',
+            'Description' => 'Seedance 2.0 默认素材组',
+        ]);
+
+        $result = $responseData['Result'] ?? [];
+        $groupId = $result['GroupId'] ?? $result['Id'] ?? '';
+        if (empty($groupId)) {
+            throw new \Exception('创建Seedance 2.0素材组失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE));
+        }
+
+        logDB('generate', 'info', 'Seedance2.0素材组创建成功', ['group_id' => $groupId]);
+        return $groupId;
+    }
+
+    /**
+     * 调用百度AI网关素材接口
+     *
+     * @param string $action 接口Action(CreateAssetGroup/CreateAsset/GetAsset等)
+     * @param array $body 请求体
+     * @return array 响应数组
+     */
+    private function requestSeedance20Gateway(string $action, array $body): array
+    {
+        $apiKey = env('BAIDU_SEEDANCE_20_AK');
+        if (empty($apiKey)) {
+            throw new \Exception('百度Seedance 2.0 API KEY未配置,请检查环境变量BAIDU_SEEDANCE_20_AK');
+        }
+
+        $client = new Client(['verify' => false, 'timeout' => 120]);
+        $response = $client->post('https://ai-gateway.baidubce.com/?Action=' . $action . '&Version=2024-01-01', [
+            'headers' => [
+                'Authorization' => 'Bearer ' . $apiKey,
+                'Content-Type' => 'application/json',
+            ],
+            'json' => $body,
+        ]);
+
+        $responseData = json_decode($response->getBody(), true);
+        dLog('generate')->info('Seedance 2.0 网关 ' . $action . ' 响应: ', $responseData);
+        logDB('generate', 'info', 'Seedance2.0网关' . $action, ['params' => $body, 'response' => $responseData]);
+
+        return $responseData;
+    }
+
     // 更新Seedance视频任务状态
     private function updateSeedanceTask($task) {
         $statusInfo = $this->querySeedanceTaskStatus($task);
@@ -719,19 +1205,23 @@ class AIVideoGenerationService
                 ];
             }
 
-            // 调用火山引擎 API 查询任务状态
-            $apiKey = env('VOLC_AI_API_KEY');
-                
+            // 调用 API 查询任务状态(Seedance 2.0走百度AI网关,其余走火山引擎)
+            $extraParams = is_array($task->extra_params) ? $task->extra_params : json_decode($task->extra_params, true);
+            $gatewayBaseUrl = $extraParams['gateway_base_url'] ?? 'https://ark.cn-beijing.volces.com';
+            $apiKeyEnv = $extraParams['api_key_env'] ?? 'VOLC_AI_API_KEY';
+
+            $apiKey = env($apiKeyEnv);
+
             if (empty($apiKey)) {
                 return [
                     'status'    => MpGenerateVideoTask::STATUS_FAILED,
                     'result_json' => [],
-                    'error_message' => '火山引擎访问密钥未配置'
+                    'error_message' => $apiKeyEnv . ' 未配置'
                 ];
             }
 
             // 构建查询 URL
-            $url = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/' . $task->task_id;
+            $url = $gatewayBaseUrl . '/api/v3/contents/generations/tasks/' . $task->task_id;
             
             $response = $client->get($url, [
                 'headers' => [
@@ -3236,4 +3726,4 @@ class AIVideoGenerationService
         }
     }
 }
-    
+    

+ 20 - 1
app/Services/Anime/AnimeService.php

@@ -4908,7 +4908,7 @@ class AnimeService
         
         // 如果有首帧图片,添加到content中
         if ($hasImage) {
-            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128'])) {
+            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'doubao-seedance-2.0'])) {
                 $videoParams['content'][] = [
                     'type' => 'image_url',
                     'image_url' => [
@@ -5013,6 +5013,25 @@ class AnimeService
             
             // 调用统一API创建任务
             $task = $this->aiVideoGenerationService->createUnifiedApiTask($unifiedParams);
+        } elseif (strpos($model, 'doubao-seedance-2.0') !== false) {
+            // 豆包Seedance 2.0(百度AI网关):图片需要先上传为素材
+            // 保存原始参考图用于任务记录
+            $videoParams['reference_images'] = $reference_images;
+
+            if ($reference_images) {
+                // 限制只传入9张参考图
+                $reference_images = array_slice($reference_images, 0, 9);
+
+                // 参考图上传为百度网关素材,上传失败时同步清理提示词中的<图片N>标记
+                $reference_image_assets = $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $fullPrompt);
+
+                // 素材上传失败后提示词可能被清理,同步更新视频参数中的提示词
+                $videoParams['prompt'] = trim($fullPrompt);
+                $videoParams['content'][0]['text'] = trim($fullPrompt);
+                $videoParams['reference_image_assets'] = $reference_image_assets;
+            }
+
+            $task = $this->aiVideoGenerationService->createSeedance20Task($videoParams);
         } else {
             $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams);
         }

+ 32 - 0
database/migrations/2026_08_05_000001_add_source_type_to_mp_assets_table.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddSourceTypeToMpAssetsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('mp_assets', function (Blueprint $table) {
+            $table->string('source_type', 32)->default('zhizhen')->comment('素材来源:zhizhen-智真AI, baidu-百度AI网关')->index()->after('asset_type');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('mp_assets', function (Blueprint $table) {
+            $table->dropColumn('source_type');
+        });
+    }
+}