فهرست منبع

Merge branch 'tmp0728' into test

lh 1 روز پیش
والد
کامیت
f5c9763315
3فایلهای تغییر یافته به همراه351 افزوده شده و 115 حذف شده
  1. 219 37
      app/Services/AIGeneration/AIImageGenerationService.php
  2. 125 74
      app/Services/Anime/AnimeService.php
  3. 7 4
      app/Services/DeepSeek/DeepSeekService.php

+ 219 - 37
app/Services/AIGeneration/AIImageGenerationService.php

@@ -243,15 +243,17 @@ class AIImageGenerationService
             // 更新状态为处理中
             $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING);
             
-            // $api_url = 'https://token.ithinkai.cn/v1/images/generations';
-            $api_url = 'https://api.nonelinear.com/v1/images/generations';
+            // $api_url = 'https://token.ithinkai.cn/v1/images/generations';    // 备用地址1
+            // $api_url = 'https://api.nonelinear.com/v1/images/generations';   // 备用地址2
+            $api_url = 'https://ai-api.kkidc.com/v1/images/generations';
             $isEditMode = false;
             
             // 参考图片 - 如果有参考图则使用 /edits 接口
             if ($task->ref_img_url) {
-                // $api_url = 'https://token.ithinkai.cn/v1/images/edits';
-                // $isEditMode = true;
-                $api_url = 'https://api.nonelinear.com/v1/images/generations';
+                // $api_url = 'https://token.ithinkai.cn/v1/images/edits';  // 备用地址1
+                // $api_url = 'https://api.nonelinear.com/v1/images/generations';  // 备用地址2
+                $api_url = 'https://ai-api.kkidc.com/v1/images/edits';
+                $isEditMode = true;
             }
             
             dLog('generate')->info('开始调用GPT-Image2 API', ['task_id' => $task->task_id, 'mode' => $isEditMode ? 'edit' : 'generation']);
@@ -269,40 +271,80 @@ class AIImageGenerationService
             // 根据是否有参考图选择不同的请求方式
             if ($isEditMode) {
                 // 使用原生 cURL 处理 multipart/form-data 格式
-                $imageUrl = is_array($task->ref_img_url) ? $task->ref_img_url[0] : $task->ref_img_url;
+                $imageUrls = is_array($task->ref_img_url) ? $task->ref_img_url : [$task->ref_img_url];
                 
-                dLog('generate')->info('下载参考图片', ['image_url' => $imageUrl]);
+                // 限制最多4张参考图
+                $imageUrls = array_slice($imageUrls, 0, 4);
                 
-                // 下载图片内容
-                $imageContent = @file_get_contents($imageUrl);
-                if ($imageContent === false) {
-                    throw new \Exception('下载参考图片失败: ' . $imageUrl);
-                }
-                
-                dLog('generate')->info('参考图片下载完成', ['size' => strlen($imageContent)]);
+                dLog('generate')->info('下载参考图片', ['image_urls' => $imageUrls, 'count' => count($imageUrls)]);
                 
-                // 保存为临时文件(cURL 需要真实文件路径)
-                $extension = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'png';
-                $tempFile = tempnam(sys_get_temp_dir(), 'gpt_img_') . '.' . $extension;
-                file_put_contents($tempFile, $imageContent);
+                // 下载所有参考图片并保存为临时文件
+                $tempFiles = [];
+                foreach ($imageUrls as $index => $imageUrl) {
+                    $imageContent = @file_get_contents($imageUrl);
+                    if ($imageContent === false) {
+                        dLog('generate')->warning('下载参考图片失败,跳过', ['image_url' => $imageUrl]);
+                        continue;
+                    }
+                    
+                    dLog('generate')->info('参考图片下载完成', ['index' => $index, 'size' => strlen($imageContent)]);
+                    
+                    // 保存为临时文件(cURL 需要真实文件路径)
+                    $extension = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'png';
+                    $tempFile = tempnam(sys_get_temp_dir(), 'gpt_img_') . '.' . $extension;
+                    file_put_contents($tempFile, $imageContent);
+                    
+                    $tempFiles[] = [
+                        'path' => $tempFile,
+                        'extension' => $extension
+                    ];
+                    
+                    dLog('generate')->info('临时文件创建成功', ['file' => $tempFile, 'size' => filesize($tempFile)]);
+                }
                 
-                dLog('generate')->info('临时文件创建成功', ['file' => $tempFile, 'size' => filesize($tempFile)]);
+                if (empty($tempFiles)) {
+                    throw new \Exception('所有参考图片下载失败');
+                }
                 
                 // 构建 POST 字段
                 $postFields = [
-                    'image' => new \CURLFile($tempFile, 'image/' . $extension, 'image.' . $extension),
                     'prompt' => $task->prompt,
                     'model' => 'gpt-image-2',
-                    'n' => (int)$task->image_num,
+                    'n' => (string)$task->image_num, // 新接口要求字符串类型
+                    'response_format' => 'b64_json', // 新接口必填参数
                 ];
                 
-                // 尺寸参数
+                // 尺寸参数(必填)
                 if ($task->width && $task->height) {
                     $postFields['size'] = $task->width . 'x' . $task->height;
+                } else {
+                    $postFields['size'] = '1024x1024'; // 默认尺寸
+                }
+                
+                // 添加参考图片文件(支持多图,新接口要求使用 image 字段名)
+                foreach ($tempFiles as $index => $tempFile) {
+                    $fieldName = 'image'; // 新接口统一使用 image 字段名
+                    $postFields[$fieldName] = new \CURLFile(
+                        $tempFile['path'], 
+                        'image/' . $tempFile['extension'], 
+                        'image' . ($index + 1) . '.' . $tempFile['extension']
+                    );
                 }
                 
-                dLog('generate')->info('GPT-Image2 API参数(编辑模式-cURL)', ['api_url' => $api_url, 'prompt' => $task->prompt, 'size' => ($task->width . 'x' . $task->height), 'n' => (int)$task->image_num]);
-                logDB('generate', 'info', 'GPT-Image2任务提交(编辑模式)', ['task_id' => $task->task_id, 'api_url' => $api_url, 'image_url' => $imageUrl, 'prompt' => $task->prompt]);
+                dLog('generate')->info('GPT-Image2 API参数(编辑模式-cURL)', [
+                    'api_url' => $api_url, 
+                    'prompt' => $task->prompt, 
+                    'size' => $postFields['size'], 
+                    'n' => $postFields['n'],
+                    'response_format' => $postFields['response_format'],
+                    'image_count' => count($tempFiles)
+                ]);
+                logDB('generate', 'info', 'GPT-Image2任务提交(编辑模式)', [
+                    'task_id' => $task->task_id, 
+                    'api_url' => $api_url, 
+                    'image_urls' => $imageUrls, 
+                    'prompt' => $task->prompt
+                ]);
                 
                 $startTime = microtime(true);
                 
@@ -333,8 +375,10 @@ class AIImageGenerationService
                 $curlErrno = curl_errno($ch);
                 curl_close($ch);
                 
-                // 删除临时文件
-                @unlink($tempFile);
+                // 删除所有临时文件
+                foreach ($tempFiles as $tempFile) {
+                    @unlink($tempFile['path']);
+                }
                 
                 $elapsed = round(microtime(true) - $startTime, 2);
                 dLog('generate')->info('cURL请求完成', ['elapsed' => $elapsed . 's', 'http_code' => $statusCode, 'curl_errno' => $curlErrno]);
@@ -352,19 +396,16 @@ class AIImageGenerationService
                     'model' => 'gpt-image-2',
                     'prompt' => $task->prompt,
                     'n' => (int)$task->image_num,
+                    'quality' => 'auto', // 新接口必填参数:图像质量等级
+                    'response_format' => 'b64_json', // 新接口必填参数:返回格式
                 ];
                 
                 // 尺寸参数
                 if ($task->width && $task->height) {
                     $apiParams['size'] = $task->width . 'x' . $task->height;
+                } else {
+                    $apiParams['size'] = 'auto'; // 新接口必填参数
                 }
-
-                if ($task->ref_img_url) {
-                    $imageUrl = is_array($task->ref_img_url) ? $task->ref_img_url[0] : $task->ref_img_url;
-                    if ($imageUrl) {
-                        $apiParams['image'] = $imageUrl;
-                    }
-                }                
                 
                 dLog('generate')->info('GPT-Image2 API参数', $apiParams);
                 logDB('generate', 'info', 'GPT-Image2任务提交', ['task_id' => $task->task_id, 'api_url' => $api_url, 'params' => $apiParams]);
@@ -453,8 +494,35 @@ class AIImageGenerationService
             }
 
             $result_urls = [];
-            foreach ($responseData['data'] as $imageData) {
-                if (isset($imageData['url'])) {
+            foreach ($responseData['data'] as $index => $imageData) {
+                // 优先处理 b64_json 格式(新接口返回格式)
+                if (isset($imageData['b64_json']) && !empty($imageData['b64_json'])) {
+                    $base64Data = $imageData['b64_json'];
+                    $imageStream = base64_decode($base64Data);
+                    
+                    if ($imageStream === false) {
+                        dLog('generate')->error('Base64解码失败', ['task_id' => $task->task_id, 'index' => $index]);
+                        continue;
+                    }
+                    
+                    $pic_name = 'ai_generation_gpt_' . time() . '_' . uniqid() . '.png';
+                    
+                    // 压缩PNG图片流
+                    $compressedData = $this->compressImageStream($imageStream);
+                    $url = uploadStreamByTos('image', $compressedData, $pic_name);
+                    
+                    $result_urls[] = $url;
+                    
+                    // 记录优化后的提示词(如果有)
+                    if (isset($imageData['revised_prompt'])) {
+                        dLog('generate')->info('图片生成优化提示词', [
+                            'task_id' => $task->task_id,
+                            'index' => $index,
+                            'revised_prompt' => $imageData['revised_prompt']
+                        ]);
+                    }
+                } elseif (isset($imageData['url']) && !empty($imageData['url'])) {
+                    // 兼容旧的 url 格式(如果接口仍返回)
                     $url = $imageData['url'];
                     $pic_name = 'ai_generation_gpt_' . time() . '_' . uniqid();
                     $pic_ext = getImgExtFromUrl($url);
@@ -464,7 +532,6 @@ class AIImageGenerationService
                         $comporessed_data = compressRemoteImageUrlToSize($url);
                         $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
                     } else {
-                        // 将图片另存到tos
                         $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
                     }
                     
@@ -480,10 +547,19 @@ class AIImageGenerationService
                 return;
             }
 
+            // 移除 b64_json 字段以避免存储大量 Base64 数据
+            $cleanedResponseData = $responseData;
+            if (isset($cleanedResponseData['data'])) {
+                foreach ($cleanedResponseData['data'] as &$imageData) {
+                    unset($imageData['b64_json']); // 移除 Base64 数据
+                }
+                unset($imageData); // 解除引用
+            }
+
             // 更新任务状态为成功
             $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
                 'result_url' => $result_urls,
-                'result_json' => $responseData
+                'result_json' => $cleanedResponseData // 保存清理后的数据
             ]);
 
             // 同步调整分镜图片状态和结果
@@ -1596,4 +1672,110 @@ class AIImageGenerationService
 
         return 'auto';
     }
+
+    /**
+     * 压缩图片流(针对PNG格式)
+     *
+     * @param string $imageStream 图片二进制数据
+     * @param int $maxBytes 最大字节数(默认3MB)
+     * @return string|null 压缩后的图片二进制数据,失败时返回原数据
+     */
+    private function compressImageStream(string $imageStream, int $maxBytes = 3 * 1024 * 1024): ?string
+    {
+        try {
+            // 检查图片类型
+            $imgInfo = @getimagesizefromstring($imageStream);
+            if (!$imgInfo) {
+                dLog('generate')->warning('无法识别图片类型,返回原始数据');
+                return $imageStream;
+            }
+
+            $mime = $imgInfo['mime'] ?? '';
+            
+            // 只处理PNG,JPEG不做有损压缩
+            if ($mime === 'image/jpeg') {
+                return $imageStream;
+            }
+
+            // 如果已经小于最大限制,直接返回
+            if (strlen($imageStream) <= $maxBytes) {
+                return $imageStream;
+            }
+
+            // 载入图像对象
+            $srcImg = @imagecreatefromstring($imageStream);
+            if (!$srcImg) {
+                dLog('generate')->warning('无法创建图像资源,返回原始数据');
+                return $imageStream;
+            }
+
+            $origW = imagesx($srcImg);
+            $origH = imagesy($srcImg);
+
+            // 逐步降低质量直到满足大小要求
+            $quality = 9; // PNG压缩等级 0-9
+            $scaleFactor = 1.0;
+            $maxAttempts = 10;
+            $attempt = 0;
+            $compressedData = null;
+
+            while ($attempt < $maxAttempts) {
+                $attempt++;
+
+                // 如果需要缩放
+                if ($scaleFactor < 1.0) {
+                    $newW = (int)($origW * $scaleFactor);
+                    $newH = (int)($origH * $scaleFactor);
+                    $resizedImg = imagecreatetruecolor($newW, $newH);
+                    
+                    // 保持透明度
+                    imagealphablending($resizedImg, false);
+                    imagesavealpha($resizedImg, true);
+                    
+                    imagecopyresampled($resizedImg, $srcImg, 0, 0, 0, 0, $newW, $newH, $origW, $origH);
+                    $targetImg = $resizedImg;
+                } else {
+                    $targetImg = $srcImg;
+                }
+
+                // 输出到内存
+                ob_start();
+                imagepng($targetImg, null, $quality);
+                $compressedData = ob_get_clean();
+
+                if ($scaleFactor < 1.0) {
+                    imagedestroy($targetImg);
+                }
+
+                // 检查大小
+                $size = strlen($compressedData);
+                dLog('generate')->info('图片压缩尝试', [
+                    'attempt' => $attempt,
+                    'quality' => $quality,
+                    'scale' => $scaleFactor,
+                    'size' => $size,
+                    'target' => $maxBytes
+                ]);
+
+                if ($size <= $maxBytes) {
+                    break;
+                }
+
+                // 调整参数
+                if ($quality > 0) {
+                    $quality--;
+                } else {
+                    $scaleFactor *= 0.9; // 每次缩小10%
+                }
+            }
+
+            imagedestroy($srcImg);
+            
+            return $compressedData ?: $imageStream;
+
+        } catch (\Exception $e) {
+            dLog('generate')->error('图片压缩失败', ['error' => $e->getMessage()]);
+            return $imageStream;
+        }
+    }
 }

+ 125 - 74
app/Services/Anime/AnimeService.php

@@ -5732,15 +5732,20 @@ class AnimeService
         $parent_id = getProp($data, 'parent_id', 0);
         $product_name = getProp($data, 'product_name');
         $product = getProp($data, 'product'); // 1.角色 2.场景 3.道具
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
+        
         if (!$product) {
             Utils::throwError('20003:请选择产品类型');
         }
         
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
+        
         // 如果有搜索条件(product_name 或 id),按条件搜索文件夹和产品
         if ($id || $product_name) {
             $query = DB::table('mp_products')
                 ->where('cpid', $cpid)
-                ->where('user_id', $uid)
+                ->where('user_id', $query_uid)
                 ->where('is_deleted', 0);
             
             // 如果指定了 id,按 id 精确查询
@@ -5777,7 +5782,7 @@ class AnimeService
         }
         
         // 递归获取所有子目录和角色
-        return $this->getChildrenRecursive($cpid, $parent_id, $product, $uid);
+        return $this->getChildrenRecursive($cpid, $parent_id, $product, $query_uid);
     }
     
     /**
@@ -5796,7 +5801,7 @@ class AnimeService
             ->where('parent_id', $parent_id);
         
         // 添加用户ID验证
-        if ($uid) {
+        if ($uid !== '') {
             $query->where('user_id', $uid);
         }
         
@@ -5850,6 +5855,10 @@ class AnimeService
         $cpid = Site::getCpid();
         $parent_id = getProp($data, 'parent_id', 0);
         $folder_name = getProp($data, 'product_name', '新建文件夹');
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
+        
+        // 根据is_public参数确定存储的user_id(公共库user_id=0,cpid保持不变)
+        $store_uid = $is_public ? 0 : $uid;
         
         // 获取 product 字段:1.角色 2.场景 3.道具,默认为1
         $product = getProp($data, 'product');
@@ -5862,6 +5871,8 @@ class AnimeService
         if ($parent_id > 0) {
             $parent = DB::table('mp_products')
                 ->where('id', $parent_id)
+                ->where('cpid', $cpid)
+                ->where('user_id', $store_uid)
                 ->where('type', 2)
                 ->where('is_deleted', 0)
                 ->first();
@@ -5886,6 +5897,8 @@ class AnimeService
         // 检查当前目录下是否已存在空白文件夹
         $empty_folder_exists = DB::table('mp_products')
             ->where('parent_id', $parent_id)
+            ->where('cpid', $cpid)
+            ->where('user_id', $store_uid)
             ->where('type', 2)
             ->where('product', $product)
             ->where('product_name', '新建文件夹')
@@ -5898,7 +5911,7 @@ class AnimeService
         
         // 创建文件夹
         $folder_id = DB::table('mp_products')->insertGetId([
-            'user_id' => $uid,
+            'user_id' => $store_uid,
             'cpid' => $cpid,
             'type' => 2,
             'parent_id' => $parent_id,
@@ -5931,15 +5944,20 @@ class AnimeService
         $cpid = Site::getCpid();
         $id = getProp($data, 'id');
         $new_name = getProp($data, 'product_name');
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
         
         if (!$id || !$new_name) {
             Utils::throwError('20003:参数不完整');
         }
         
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
+        
         // 检查是否存在
         $item = DB::table('mp_products')
             ->where('id', $id)
             ->where('cpid', $cpid)
+            ->where('user_id', $query_uid)
             ->where('is_deleted', 0)
             ->first();
         
@@ -5950,6 +5968,7 @@ class AnimeService
         return DB::table('mp_products')
             ->where('id', $id)
             ->where('cpid', $cpid)
+            ->where('user_id', $query_uid)
             ->update([
                 'product_name' => $new_name,
                 'updated_at' => date('Y-m-d H:i:s')
@@ -5962,18 +5981,24 @@ class AnimeService
      * @return bool
      */
     public function moveProductOrFolder($data) {
+        $uid = Site::getUid();
         $cpid = Site::getCpid();
         $id = getProp($data, 'id');
         $target_parent_id = getProp($data, 'target_parent_id', 0);
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
         
         if (!$id) {
             Utils::throwError('20003:请选择要移动的项目');
         }
         
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
+        
         // 检查要移动的项目
         $item = DB::table('mp_products')
             ->where('id', $id)
             ->where('cpid', $cpid)
+            ->where('user_id', $query_uid)
             ->where('is_deleted', 0)
             ->first();
         
@@ -5989,6 +6014,7 @@ class AnimeService
             $target_parent = DB::table('mp_products')
                 ->where('id', $target_parent_id)
                 ->where('cpid', $cpid)
+                ->where('user_id', $query_uid)
                 ->where('type', 2)
                 ->where('is_deleted', 0)
                 ->first();
@@ -6128,7 +6154,10 @@ class AnimeService
      * @return array 返回路径数组
      */
     public function getFolderPath($data) {
+        $uid = Site::getUid();
+        $cpid = Site::getCpid();
         $folder_id = getProp($data, 'id', 0);
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
         
         if ($folder_id == 0) {
             return [
@@ -6136,6 +6165,9 @@ class AnimeService
             ];
         }
         
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
+        
         $path = [];
         $current_id = $folder_id;
 
@@ -6143,6 +6175,8 @@ class AnimeService
         while ($current_id > 0) {
             $folder = DB::table('mp_products')
                 ->where('id', $current_id)
+                ->where('cpid', $cpid)
+                ->where('user_id', $query_uid)
                 ->where('type', 2)
                 ->where('is_deleted', 0)
                 ->first();
@@ -6168,6 +6202,11 @@ class AnimeService
     public function createProduct($data) {
         $uid = Site::getUid();
         $cpid = Site::getCpid();
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
+        
+        // 根据is_public参数确定存储的user_id(公共库user_id=0,cpid保持不变)
+        $store_uid = $is_public ? 0 : $uid;
+        
         $product_name = trim(getProp($data, 'product_name'));
         if (!$product_name) Utils::throwError('20003:名称不能为空');
         $product_name = preg_replace('/^[\s*\[\]【】[]{}{}\x{3000}]+|[\s*\[\]【】[]{}{}\x{3000}]+$/u', '', $product_name);
@@ -6202,6 +6241,7 @@ class AnimeService
             $parent = DB::table('mp_products')
                 ->where('id', $parent_id)
                 ->where('cpid', $cpid)
+                ->where('user_id', $store_uid)
                 ->where('type', 2)
                 ->where('is_deleted', 0)
                 ->first();
@@ -6214,7 +6254,7 @@ class AnimeService
         }
 
         $id = DB::table('mp_products')->insertGetId([
-            'user_id'       => $uid,
+            'user_id'       => $store_uid,
             'cpid'          => $cpid,
             'type'          => 1, // 1=角色图片
             'parent_id'     => $parent_id,
@@ -6249,11 +6289,16 @@ class AnimeService
         $id = getProp($data, 'id');
         if (!$id) Utils::throwError('20003:ID不能为空');
         $versions = getProp($data, 'versions');
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
         
-        // 检查是否存在且属于当前用户
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
+        
+        // 检查是否存在且属于当前用户或公共库
         $role = DB::table('mp_products')
         ->where('id', $id)
         ->where('cpid', $cpid)
+        ->where('user_id', $query_uid)
         ->where('type', 1)->first();
         if (!$role) Utils::throwError('20003:记录不存在');
         
@@ -6467,16 +6512,22 @@ class AnimeService
         $id = getProp($data, 'id');
         if (!$id) Utils::throwError('20003:ID不能为空');
         $ids = explode(',', $id);
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
+        
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
         
-        // 检查是否存在且属于当前用户
+        // 检查是否存在且属于当前用户或公共库
         $role = DB::table('mp_products')
         ->whereIn('id', $ids)
         ->where('cpid', $cpid)
+        ->where('user_id', $query_uid)
         ->get();
         if (!$role) Utils::throwError('20003:记录不存在');
         
         return DB::table('mp_products')
         ->where('cpid', $cpid)
+        ->where('user_id', $query_uid)
         ->whereIn('id', $ids)
         ->update([
             'is_deleted' => 1,
@@ -6489,11 +6540,16 @@ class AnimeService
         $cpid = Site::getCpid();
         $id = getProp($data, 'id');
         if (!$id) Utils::throwError('20003:ID不能为空');
+        $is_public = getProp($data, 'is_public', 0); // 0=个人资产库 1=公共资产库
+        
+        // 根据is_public参数确定查询的user_id(公共库user_id=0,cpid保持不变)
+        $query_uid = $is_public ? 0 : $uid;
         
-        // 检查是否存在且属于当前用户
+        // 检查是否存在且属于当前用户或公共库
         $product = DB::table('mp_products')
         ->where('id', $id)
         ->where('cpid', $cpid)
+        ->where('user_id', $query_uid)
         ->where('type', 1)->first();
         if (!$product) Utils::throwError('20003:资产不存在');
         
@@ -6679,6 +6735,67 @@ class AnimeService
         if (!$script) {
             Utils::throwError('20003:剧本不存在');
         }
+
+        // 处理图片模型
+        $model = getProp($data, 'model');
+        if (!$model) {
+            // 使用默认模型
+            $model = 'doubao-seedream-5-0-lite-260128';
+        }
+        // 验证模型是否在可用模型表中
+        if (!DB::table('mp_image_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
+            Utils::throwError('20003:该模型已不可用,请更换!');
+        }
+        $ratio = getProp($data, 'ratio', '9:16');
+        $resolution = getProp($data, 'resolution', '2k');
+
+        // 定义分辨率和宽高比对应的尺寸映射表
+        $sizeMap = [
+            '2k' => [
+                '1:1' => ['width' => 2048, 'height' => 2048],
+                '4:3' => ['width' => 2304, 'height' => 1728],
+                '3:4' => ['width' => 1728, 'height' => 2304],
+                '16:9' => ['width' => 2848, 'height' => 1600],
+                '9:16' => ['width' => 1600, 'height' => 2848],
+                '3:2' => ['width' => 2496, 'height' => 1664],
+                '2:3' => ['width' => 1664, 'height' => 2496],
+                '21:9' => ['width' => 3136, 'height' => 1344],
+            ],
+            '3k' => [
+                '1:1' => ['width' => 3072, 'height' => 3072],
+                '4:3' => ['width' => 3456, 'height' => 2592],
+                '3:4' => ['width' => 2592, 'height' => 3456],
+                '16:9' => ['width' => 4096, 'height' => 2304],
+                '9:16' => ['width' => 2304, 'height' => 4096],
+                '2:3' => ['width' => 2496, 'height' => 3744],
+                '3:2' => ['width' => 3744, 'height' => 2496],
+                '21:9' => ['width' => 4704, 'height' => 2016],
+            ],
+            '4k' => [
+                '1:1' => ['width' => 4096, 'height' => 4096],
+                '3:4' => ['width' => 3520, 'height' => 4704],
+                '4:3' => ['width' => 4704, 'height' => 3520],
+                '16:9' => ['width' => 5504, 'height' => 3040],
+                '9:16' => ['width' => 3040, 'height' => 5504],
+                '2:3' => ['width' => 3328, 'height' => 4992],
+                '3:2' => ['width' => 4992, 'height' => 3328],
+                '21:9' => ['width' => 6240, 'height' => 2656],
+            ],
+        ];
+        
+        // 参数验证
+        $resolution = strtolower($resolution);
+        if (!isset($sizeMap[$resolution])) {
+            Utils::throwError('20003:分辨率参数错误,仅支持 2k、3k、4k');
+        }
+        
+        if (!isset($sizeMap[$resolution][$ratio])) {
+            Utils::throwError('20003:宽高比参数错误,该分辨率下不支持 ' . $ratio . ' 比例');
+        }
+        
+        // 根据 ratio 和 resolution 确定宽高
+        $width = $sizeMap[$resolution][$ratio]['width'];
+        $height = $sizeMap[$resolution][$ratio]['height'];
         
         $script_name = $script->script_name ?? 'script_' . $script_id;
         
@@ -6745,67 +6862,6 @@ class AnimeService
                     'script_name' => $script_name
                 ]);
 
-                // 处理图片模型
-                $model = getProp($data, 'model');
-                if (!$model) {
-                    // 使用默认模型
-                    $model = 'doubao-seedream-5-0-lite-260128';
-                }
-                // 验证模型是否在可用模型表中
-                if (!DB::table('mp_image_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
-                    Utils::throwError('20003:该模型已不可用,请更换!');
-                }
-                $ratio = getProp($data, 'ratio', '16:9');
-                $resolution = getProp($data, 'resolution', '2k');
-
-                // 定义分辨率和宽高比对应的尺寸映射表
-                $sizeMap = [
-                    '2k' => [
-                        '1:1' => ['width' => 2048, 'height' => 2048],
-                        '4:3' => ['width' => 2304, 'height' => 1728],
-                        '3:4' => ['width' => 1728, 'height' => 2304],
-                        '16:9' => ['width' => 2848, 'height' => 1600],
-                        '9:16' => ['width' => 1600, 'height' => 2848],
-                        '3:2' => ['width' => 2496, 'height' => 1664],
-                        '2:3' => ['width' => 1664, 'height' => 2496],
-                        '21:9' => ['width' => 3136, 'height' => 1344],
-                    ],
-                    '3k' => [
-                        '1:1' => ['width' => 3072, 'height' => 3072],
-                        '4:3' => ['width' => 3456, 'height' => 2592],
-                        '3:4' => ['width' => 2592, 'height' => 3456],
-                        '16:9' => ['width' => 4096, 'height' => 2304],
-                        '9:16' => ['width' => 2304, 'height' => 4096],
-                        '2:3' => ['width' => 2496, 'height' => 3744],
-                        '3:2' => ['width' => 3744, 'height' => 2496],
-                        '21:9' => ['width' => 4704, 'height' => 2016],
-                    ],
-                    '4k' => [
-                        '1:1' => ['width' => 4096, 'height' => 4096],
-                        '3:4' => ['width' => 3520, 'height' => 4704],
-                        '4:3' => ['width' => 4704, 'height' => 3520],
-                        '16:9' => ['width' => 5504, 'height' => 3040],
-                        '9:16' => ['width' => 3040, 'height' => 5504],
-                        '2:3' => ['width' => 3328, 'height' => 4992],
-                        '3:2' => ['width' => 4992, 'height' => 3328],
-                        '21:9' => ['width' => 6240, 'height' => 2656],
-                    ],
-                ];
-                
-                // 参数验证
-                $resolution = strtolower($resolution);
-                if (!isset($sizeMap[$resolution])) {
-                    Utils::throwError('20003:分辨率参数错误,仅支持 2k、3k、4k');
-                }
-                
-                if (!isset($sizeMap[$resolution][$ratio])) {
-                    Utils::throwError('20003:宽高比参数错误,该分辨率下不支持 ' . $ratio . ' 比例');
-                }
-                
-                // 根据 ratio 和 resolution 确定宽高
-                $width = $sizeMap[$resolution][$ratio]['width'];
-                $height = $sizeMap[$resolution][$ratio]['height'];
-                
                 // 开启事务
                 DB::beginTransaction();
                 
@@ -6932,11 +6988,6 @@ class AnimeService
             // 存储每个类型的根文件夹ID
             $typeFolderIds = [];
             
-            // 默认图片生成参数
-            $model = 'doubao-seedream-5-0-lite-260128';
-            $width = 1600;
-            $height = 2848;
-            
             // 遍历每个产品类型(type: 1=角色, 2=场景, 3=道具)
             foreach ($products as $productGroup) {
                 $type = getProp($productGroup, 'type');

+ 7 - 4
app/Services/DeepSeek/DeepSeekService.php

@@ -1321,10 +1321,12 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
         }
         $post_data['max_completion_tokens'] = 100000;
 
-        // 备用中转站地址: https://token.ithinkai.cn/v1/chat/completions
-        $response = $client->post('https://api.nonelinear.com/v1/chat/completions', [
+        // 备用中转站地址1: https://token.ithinkai.cn/v1/chat/completions
+        // 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
+        $response = $client->post('https://ai-api.kkidc.com/v1/chat/completions', [
             'json' => $post_data, 
-            'headers' => $headers
+            'headers' => $headers,
+            'stream' => true  // 启用流式响应
         ]);
         
         $body = $response->getBody();
@@ -11169,7 +11171,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
         $post_data['stream'] = false;
 
         // 备用中转站地址: https://token.ithinkai.cn/v1/chat/completions
-        $result = $client->post('https://api.nonelinear.com/v1/chat/completions', [
+        // 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
+        $result = $client->post('https://ai-api.kkidc.com/v1/chat/completions', [
             'json' => $post_data, 
             'headers' => $headers
         ]);