unknown 5 дней назад
Родитель
Сommit
03a3136db4
1 измененных файлов с 93 добавлено и 100 удалено
  1. 93 100
      application/api/controller/WorkOrder.php

+ 93 - 100
application/api/controller/WorkOrder.php

@@ -262,15 +262,14 @@ class WorkOrder extends Api{
     /**
      * 即梦AI--创建视频任务接口
      * 支持:单张首帧图 / 首尾双帧图
-     * 图片入参:form-data 文件(first_image/last_image)、base64、或 http(s) URL
+     * 图片入参:JSON/base64(first_image/last_image)、form-data 文件、或 http(s) URL
      */
     public function Create_ImgToVideo()
     {
         $apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
         $apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
 
-        $params = $this->request->param();
-        // halt($params);
+        $params = $this->mergeRequestParams();
         $prompt = trim((string)($params['prompt'] ?? ''));
         if ($prompt === '') {
             return json(['code' => 0, 'msg' => 'prompt 不能为空']);
@@ -280,10 +279,18 @@ class WorkOrder extends Api{
         $firstFrame = $this->resolveFrameImagePayload($params, 'first', $firstError);
         if (($firstFrame['api_url'] ?? '') === '') {
             $hint = Common::isOssEnabled()
-                ? '请用 form-data 上传 first_image(类型选文件),并查看 runtime/log'
+                ? '请传 first_image(JSON 内 data:image/...;base64,... 或 form-data 文件),并查看 runtime/log'
                 : '请在 application/config.php 配置 oss(accessKeyId、endpoint、bucket、host)';
             $detail = $firstError !== '' ? '(' . $firstError . ')' : '';
-            return json(['code' => 0, 'msg' => '首帧图片无效或上传 OSS 失败。' . $hint . $detail]);
+            return json([
+                'code' => 0,
+                'msg' => '首帧图片无效或上传失败。' . $hint . $detail,
+                'data' => [
+                    'has_first_image' => isset($params['first_image']) && $params['first_image'] !== '',
+                    'content_type' => $this->request->contentType(),
+                    'upload_env' => $this->getPhpUploadEnvInfo(),
+                ],
+            ]);
         }
 
         $lastFrame = $this->resolveFrameImagePayload($params, 'last');
@@ -295,8 +302,8 @@ class WorkOrder extends Api{
             'model' => 'doubao-seedance-1-5-pro-251215',
             'content' => $content,
             'generate_audio' => filter_var($params['generate_audio'] ?? true, FILTER_VALIDATE_BOOLEAN),
-            'ratio' => $params['ratio'] ?? $params['aspect_ratio'] ?? 'adaptive',
-            'duration' => (int)($params['duration'] ?? 5),
+            'ratio' => $params['ratio'] ?? $params['aspect_ratio'] ?? $params['size'] ?? 'adaptive',
+            'duration' => (int)($params['duration'] ?? $params['seconds'] ?? 5),
             'watermark' => filter_var($params['watermark'] ?? false, FILTER_VALIDATE_BOOLEAN),
         ];
 
@@ -1410,11 +1417,6 @@ class WorkOrder extends Api{
             if ($raw === '' || strlen($raw) < 50) {
                 continue;
             }
-            preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $raw, $bm);
-            if (empty($bm)) {
-                $error = ($prefix === 'first' ? '首帧图' : '尾帧图') . '未找到图片数据';
-                continue;
-            }
             $result = $this->uploadBase64ImageToOss($raw, $prefix, $ossTaskId, $error);
             if (($result['url'] ?? '') !== '') {
                 return [
@@ -1609,6 +1611,15 @@ class WorkOrder extends Api{
         $objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
         $upload = $this->uploadToOSS($localFullPath, $objectKey);
         if (!$upload['success']) {
+            $fallbackUrl = $this->buildPublicUploadUrl($localFullPath);
+            if ($fallbackUrl !== '') {
+                Log::write('[uploadBase64ImageToOss] OSS失败,回退本站URL: ' . $fallbackUrl, 'warning');
+                return [
+                    'url' => $fallbackUrl,
+                    'local_path' => $localFullPath,
+                    'object_key' => $objectKey,
+                ];
+            }
             Log::write('[uploadBase64ImageToOss] OSS上传失败: ' . $objectKey, 'error');
             return $empty;
         }
@@ -1622,7 +1633,7 @@ class WorkOrder extends Api{
     }
 
     /**
-     * 解析 base64 图片(与 ImageToImageJob 一致:data:image/(png|jpg|jpeg);base64,...
+     * 解析 base64 图片(避免对大字符串整段正则,防止服务器 PCRE 超限
      * @return array{0:string,1:string}|null [扩展名, 二进制内容]
      */
     private function parseBase64Image(string $base64Input): ?array
@@ -1632,15 +1643,30 @@ class WorkOrder extends Api{
             return null;
         }
 
-        preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $base64Input, $m);
-        if (empty($m)) {
+        $ext = 'jpg';
+        $rawBase64 = $base64Input;
+        $prefix = 'data:image/';
+        if (stripos($base64Input, $prefix) === 0) {
+            $semi = stripos($base64Input, ';base64,');
+            if ($semi === false) {
+                return null;
+            }
+            $mimePart = strtolower(substr($base64Input, strlen($prefix), $semi - strlen($prefix)));
+            if ($mimePart === 'jpeg') {
+                $mimePart = 'jpg';
+            }
+            if (in_array($mimePart, ['jpg', 'png', 'gif', 'webp', 'bmp'], true)) {
+                $ext = $mimePart;
+            }
+            $rawBase64 = substr($base64Input, $semi + 8);
+        }
+
+        $rawBase64 = preg_replace('/\s+/', '', $rawBase64);
+        if ($rawBase64 === '') {
             return null;
         }
 
-        $rawBase64 = preg_replace('/\s+/', '', $m[2]);
-        $ext = $m[1];
         $imageData = base64_decode($rawBase64, true);
-
         if ($imageData === false || strlen($imageData) < 100) {
             return null;
         }
@@ -1648,6 +1674,29 @@ class WorkOrder extends Api{
         return [$ext, $imageData];
     }
 
+    /**
+     * 合并请求参数(兼容浏览器 application/json 提交 first_image/last_image base64)
+     */
+    private function mergeRequestParams(): array
+    {
+        $params = $this->request->param();
+        if (!empty($params['first_image']) || !empty($params['last_image']) || !empty($params['prompt'])) {
+            return $params;
+        }
+
+        $content = $this->request->getInput();
+        if (!is_string($content) || $content === '') {
+            return $params;
+        }
+
+        $json = json_decode($content, true);
+        if (!is_array($json)) {
+            return $params;
+        }
+
+        return array_merge($params, $json);
+    }
+
     /**
      * 将本地文件上传到阿里云 OSS
      *
@@ -1675,7 +1724,7 @@ class WorkOrder extends Api{
             $ossObjectKey = implode('/', $segments);
         }
 
-        $success = $this->uploadLocalFileToAliyunOss($localFullPath, $ossObjectKey);
+        $success = Common::uploadLocalFileToOss($localFullPath, $ossObjectKey);
         return [
             'success' => $success,
             'object_key' => $ossObjectKey,
@@ -1684,95 +1733,39 @@ class WorkOrder extends Api{
     }
 
     /**
-     * 上传本地文件到阿里云 OSS(仅 WorkOrder 内实现,不修改 Common
+     * 将 public 下本地文件转为可公网访问的站点 URL(OSS 不可用时的回退
      */
-    private function uploadLocalFileToAliyunOss(string $localFullPath, string $objectKey): bool
+    private function buildPublicUploadUrl(string $localFullPath): string
     {
-        if (!Common::isOssEnabled() || !is_file($localFullPath)) {
-            return false;
-        }
-        $objectKey = Common::normalizeOssObjectKey($objectKey);
-        if ($objectKey === '') {
-            return false;
-        }
-
-        if (class_exists(\OSS\OssClient::class, true)) {
-            try {
-                $config = Common::getOssConfig();
-                $client = new \OSS\OssClient(
-                    $config['accessKeyId'],
-                    $config['accessKeySecret'],
-                    $config['endpoint']
-                );
-                $client->uploadFile($config['bucket'], $objectKey, $localFullPath);
-                return true;
-            } catch (\Throwable $e) {
-                Log::write('[uploadLocalFileToAliyunOss SDK] ' . $e->getMessage() . ' | ' . $objectKey, 'error');
-            }
+        $publicRoot = str_replace('\\', '/', ROOT_PATH . 'public/');
+        $path = str_replace('\\', '/', $localFullPath);
+        if (strpos($path, $publicRoot) !== 0) {
+            return '';
         }
-
-        return $this->putLocalFileToAliyunOssByCurl($localFullPath, $objectKey);
+        $relative = ltrim(substr($path, strlen($publicRoot)), '/');
+        return $relative === '' ? '' : rtrim($this->request->domain(), '/') . '/' . $relative;
     }
 
     /**
-     * 无 OSS SDK 时通过 REST PUT 上传
+     * @return array{summary:string,content_length:int,post_max_size:string,upload_max_filesize:string,has_first_image_file:bool}
      */
-    private function putLocalFileToAliyunOssByCurl(string $localFullPath, string $objectKey): bool
+    private function getPhpUploadEnvInfo(): array
     {
-        $config = Common::getOssConfig();
-        $content = file_get_contents($localFullPath);
-        if ($content === false) {
-            return false;
-        }
-
-        $mime = function_exists('mime_content_type') ? (mime_content_type($localFullPath) ?: '') : '';
-        if ($mime === '') {
-            $ext = strtolower(pathinfo($localFullPath, PATHINFO_EXTENSION));
-            $mimeMap = [
-                'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
-                'gif' => 'image/gif', 'webp' => 'image/webp', 'mp4' => 'video/mp4',
-            ];
-            $mime = $mimeMap[$ext] ?? 'application/octet-stream';
-        }
-
-        $date = gmdate('D, d M Y H:i:s \G\M\T');
-        $bucket = $config['bucket'];
-        $endpoint = ltrim((string)$config['endpoint'], 'https://');
-        $endpoint = ltrim($endpoint, 'http://');
-        $canonicalizedResource = '/' . $bucket . '/' . $objectKey;
-        $stringToSign = "PUT\n\n{$mime}\n{$date}\n{$canonicalizedResource}";
-        $signature = base64_encode(hash_hmac('sha1', $stringToSign, $config['accessKeySecret'], true));
-        $urlPath = implode('/', array_map('rawurlencode', explode('/', $objectKey)));
-        $url = 'https://' . $bucket . '.' . $endpoint . '/' . $urlPath;
-
-        $ch = curl_init($url);
-        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
-        curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
-        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
-        curl_setopt($ch, CURLOPT_HTTPHEADER, [
-            'Date: ' . $date,
-            'Content-Type: ' . $mime,
-            'Authorization: OSS ' . $config['accessKeyId'] . ':' . $signature,
-        ]);
-        curl_setopt($ch, CURLOPT_TIMEOUT, 300);
-        $response = curl_exec($ch);
-        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
-        $curlError = curl_error($ch);
-        curl_close($ch);
-
-        if ($httpCode >= 200 && $httpCode < 300) {
-            return true;
-        }
-
-        Log::write(
-            '[putLocalFileToAliyunOssByCurl] http=' . $httpCode
-            . ' err=' . $curlError
-            . ' resp=' . substr((string)$response, 0, 500)
-            . ' | objectKey=' . $objectKey,
-            'error'
-        );
-        return false;
+        $contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
+        $postMax = (string)ini_get('post_max_size');
+        $uploadMax = (string)ini_get('upload_max_filesize');
+        $hasFile = !empty($this->request->file('first_image'));
+        $summary = 'CONTENT_LENGTH=' . $contentLength
+            . ';post_max_size=' . $postMax
+            . ';upload_max_filesize=' . $uploadMax
+            . ';has_first_image_file=' . ($hasFile ? 'yes' : 'no');
+        return [
+            'summary' => $summary,
+            'content_length' => $contentLength,
+            'post_max_size' => $postMax,
+            'upload_max_filesize' => $uploadMax,
+            'has_first_image_file' => $hasFile,
+        ];
     }
 
     /**