Quellcode durchsuchen

文生文通用接口新增对gemini大模型及参考图或参考视频的收费逻辑

lh vor 1 Monat
Ursprung
Commit
c63509362f

+ 7 - 0
app/Http/Controllers/DeepSeek/DeepSeekController.php

@@ -1043,6 +1043,13 @@ class DeepSeekController extends BaseController
             $data['images'] = [$request->file('image')];
             $data['images'] = [$request->file('image')];
         }
         }
 
 
+        // 处理上传的视频文件(仅 Gemini 模型支持)
+        if ($request->hasFile('videos')) {
+            $data['videos'] = $request->file('videos');
+        } else if ($request->hasFile('video')) {
+            $data['videos'] = [$request->file('video')];
+        }
+
         return response()->stream(function () use ($data) {
         return response()->stream(function () use ($data) {
             // 禁用所有输出缓冲
             // 禁用所有输出缓冲
             if (ob_get_level()) {
             if (ob_get_level()) {

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

@@ -145,6 +145,25 @@ class DeepSeekService
         if ((!empty($videos) || !empty($videoUrls)) && !$isGeminiModel) {
         if ((!empty($videos) || !empty($videoUrls)) && !$isGeminiModel) {
             Utils::throwError('20003:仅Gemini模型支持视频输入');
             Utils::throwError('20003:仅Gemini模型支持视频输入');
         }
         }
+
+        // Gemini 多模态(图片/视频 -> 文本)按模型预检积分;纯文本不扣费
+        $uid = Site::getUid();
+        $chargeModality = '';
+        $chargePoints = 0;
+        $hasImageMedia = !empty($images) || !empty($imageUrls)
+            || $this->messagesContainMedia($messages, ['image_url', 'image']);
+        $hasVideoMedia = !empty($videos) || !empty($videoUrls)
+            || $this->messagesContainMedia($messages, ['video_url', 'video']);
+        if ($isGeminiModel && ($hasImageMedia || $hasVideoMedia)) {
+            $chargeModality = $hasVideoMedia ? 'video' : 'image';
+            $pricingModel = $this->geminiMultimodalPricingModel($model);
+            if ($pricingModel !== '') {
+                $chargePoints = $this->pointsService->getVideoChargePoints(['model' => $pricingModel]);
+                if ($chargePoints > 0) {
+                    $this->pointsService->checkUserPointsEnough($chargePoints, $uid);
+                }
+            }
+        }
         
         
         // 如果没有提供messages,则根据system_prompt、content和images构建
         // 如果没有提供messages,则根据system_prompt、content和images构建
         if (empty($messages)) {
         if (empty($messages)) {
@@ -296,19 +315,119 @@ class DeepSeekService
         // 根据模型类型选择调用方法
         // 根据模型类型选择调用方法
         if (in_array($model, ['deepseek-reasoner', 'deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'])) {
         if (in_array($model, ['deepseek-reasoner', 'deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'])) {
             // DeepSeek 官方模型使用 DeepSeek API
             // DeepSeek 官方模型使用 DeepSeek API
-            return $this->deepSeekStreamResponse($post_data);
+            $streamGenerator = $this->deepSeekStreamResponse($post_data);
         } else if (in_array($model, $this->gpt_text_models)) {
         } else if (in_array($model, $this->gpt_text_models)) {
             // GPT 模型使用 TokenRouter API
             // GPT 模型使用 TokenRouter API
-            return $this->gpt54StreamResponse($post_data);
+            $streamGenerator = $this->gpt54StreamResponse($post_data);
         } else if (in_array($model, $this->gemini_text_models)) {
         } else if (in_array($model, $this->gemini_text_models)) {
             // Gemini 模型使用 Gemini API
             // Gemini 模型使用 Gemini API
-            return $this->geminiStreamResponse($post_data, $model);
+            $streamGenerator = $this->geminiStreamResponse($post_data, $model);
         } else if (in_array($model, $this->valid_text_models)) {
         } else if (in_array($model, $this->valid_text_models)) {
             // 火山引擎支持的模型使用火山引擎 API
             // 火山引擎支持的模型使用火山引擎 API
-            return $this->volcEngineChatCompletion($post_data);
+            $streamGenerator = $this->volcEngineChatCompletion($post_data);
         } else {
         } else {
             Utils::throwError('20003:不支持的模型: ' . $model);
             Utils::throwError('20003:不支持的模型: ' . $model);
         }
         }
+
+        if ($chargePoints > 0) {
+            return $this->wrapGenerateTextCharge($streamGenerator, $uid, $model, $chargePoints, $chargeModality);
+        }
+        return $streamGenerator;
+    }
+
+    /**
+     * 流式生成成功后再按 Gemini 多模态规则扣费
+     *
+     * @param \Generator $generator
+     * @param int        $uid
+     * @param string     $model
+     * @param int        $points
+     * @param string     $modality
+     * @return \Generator
+     */
+    private function wrapGenerateTextCharge(\Generator $generator, int $uid, string $model, int $points, string $modality): \Generator
+    {
+        $charged = false;
+        foreach ($generator as $chunk) {
+            if (!$charged && isset($chunk['type']) && $chunk['type'] === 'done') {
+                $fullContent = (string)getProp($chunk, 'full_content', getProp($chunk, 'answer', ''));
+                if ($fullContent !== '') {
+                    $this->chargeGeminiMultimodal($uid, $model, $points, $modality, getProp($chunk, 'usage', []));
+                    $charged = true;
+                }
+            }
+            yield $chunk;
+        }
+    }
+
+    /**
+     * Gemini 多模态(图片/视频 -> 文本)成功后扣费并记录 token 明细
+     *
+     * @param int    $uid
+     * @param string $model
+     * @param int    $points
+     * @param string $modality
+     * @param mixed  $usage
+     * @return void
+     */
+    private function chargeGeminiMultimodal(int $uid, string $model, int $points, string $modality, $usage): void
+    {
+        $result = $this->pointsService->recordChatCharge(
+            $uid,
+            $points,
+            $this->pointsService->getTokensFromUsage($usage),
+            [
+                'model'    => $model,
+                'modality' => $modality,
+                'source'   => 'generateText',
+            ],
+            $modality === 'video' ? 'Gemini视频理解成功计费' : 'Gemini图片理解成功计费',
+            $model
+        );
+        if (empty($result['charged'])) {
+            throw new \Exception('Gemini多模态计费失败: ' . (string)($result['reason'] ?? 'unknown'));
+        }
+    }
+
+    /**
+     * Gemini 文本模型 ID 转 mp_video_models 中的按次计费模型 ID
+     *
+     * @param string $model
+     * @return string
+     */
+    private function geminiMultimodalPricingModel(string $model): string
+    {
+        $map = [
+            'gemini-3.1-flash-image-preview-t' => 'gemini-3.1-flash',
+            'gemini-3.1-pro-preview'           => 'gemini-3.1-pro',
+            'gemini-3-flash-preview'           => 'gemini-3.0-flash',
+            'gemini-3-pro-preview'             => 'gemini-3.0-pro',
+            'gemini-3.6-flash'                 => 'gemini-3.6-flash',
+        ];
+        return $map[$model] ?? '';
+    }
+
+    /**
+     * 检查 OpenAI 风格消息中是否包含指定媒体类型
+     *
+     * @param array  $messages
+     * @param array  $mediaTypes
+     * @return bool
+     */
+    private function messagesContainMedia(array $messages, array $mediaTypes): bool
+    {
+        foreach ($messages as $message) {
+            $content = getProp($message, 'content', '');
+            if (!is_array($content)) {
+                continue;
+            }
+            foreach ($content as $part) {
+                if (is_array($part) && in_array(getProp($part, 'type', ''), $mediaTypes, true)) {
+                    return true;
+                }
+            }
+        }
+        return false;
     }
     }
 
 
     public function createGenerateText($data) {
     public function createGenerateText($data) {

+ 3 - 2
app/Services/PointsService.php

@@ -503,15 +503,16 @@ class PointsService
      * @param int $tokens
      * @param int $tokens
      * @param array $chargeInfo
      * @param array $chargeInfo
      * @param string $remark
      * @param string $remark
+     * @param string $apiType
      * @return array
      * @return array
      */
      */
-    public function recordChatCharge(int $uid, int $points, int $tokens, array $chargeInfo = [], string $remark = 'AI对话生成成功计费'): array
+    public function recordChatCharge(int $uid, int $points, int $tokens, array $chargeInfo = [], string $remark = 'AI对话生成成功计费', string $apiType = 'deepseek'): array
     {
     {
         return $this->deductAndRecord(
         return $this->deductAndRecord(
             $uid,
             $uid,
             null,
             null,
             MpUserPointsDetail::TYPE_CHAT,
             MpUserPointsDetail::TYPE_CHAT,
-            'deepseek',
+            $apiType,
             (float)$points,
             (float)$points,
             $tokens,
             $tokens,
             $chargeInfo,
             $chargeInfo,