|
@@ -10,6 +10,7 @@ use GuzzleHttp\Client;
|
|
|
use DateTime;
|
|
use DateTime;
|
|
|
use DateTimeZone;
|
|
use DateTimeZone;
|
|
|
use App\Services\AIGeneration\AIImageGenerationService;
|
|
use App\Services\AIGeneration\AIImageGenerationService;
|
|
|
|
|
+use App\Services\PointsService;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Redis;
|
|
use Illuminate\Support\Facades\Redis;
|
|
@@ -29,9 +30,11 @@ class DeepSeekService
|
|
|
private $valid_text_models;
|
|
private $valid_text_models;
|
|
|
private $gpt_text_models;
|
|
private $gpt_text_models;
|
|
|
protected $aiImageGenerationService;
|
|
protected $aiImageGenerationService;
|
|
|
|
|
+ protected $pointsService;
|
|
|
|
|
|
|
|
- public function __construct(AIImageGenerationService $aiImageGenerationService) {
|
|
|
|
|
|
|
+ public function __construct(AIImageGenerationService $aiImageGenerationService, PointsService $pointsService) {
|
|
|
$this->aiImageGenerationService = $aiImageGenerationService;
|
|
$this->aiImageGenerationService = $aiImageGenerationService;
|
|
|
|
|
+ $this->pointsService = $pointsService;
|
|
|
$this->url = 'https://api.deepseek.com/chat/completions'; // DeepSeek API请求地址
|
|
$this->url = 'https://api.deepseek.com/chat/completions'; // DeepSeek API请求地址
|
|
|
$this->api_key = env('DEEPSEEK_API_KEY');
|
|
$this->api_key = env('DEEPSEEK_API_KEY');
|
|
|
$this->headers = [
|
|
$this->headers = [
|
|
@@ -305,6 +308,29 @@ class DeepSeekService
|
|
|
'script_name' => $script_name
|
|
'script_name' => $script_name
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * AI对话调用成功后的积分扣费(返回结果前调用)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 计费失败抛错,由调用方事务回滚,避免“成功但未扣费”。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param int $uid
|
|
|
|
|
+ * @param int $tokens
|
|
|
|
|
+ * @param array $chargeInfo
|
|
|
|
|
+ * @return void
|
|
|
|
|
+ */
|
|
|
|
|
+ private function chargeChatSuccess(int $uid, int $tokens, array $chargeInfo): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $result = $this->pointsService->recordChatCharge(
|
|
|
|
|
+ $uid,
|
|
|
|
|
+ PointsService::CHAT_CHARGE_POINTS,
|
|
|
|
|
+ $tokens,
|
|
|
|
|
+ $chargeInfo
|
|
|
|
|
+ );
|
|
|
|
|
+ if (empty($result['charged'])) {
|
|
|
|
|
+ throw new \Exception('AI对话计费失败: ' . (string)($result['reason'] ?? 'unknown'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 通用文生文方法(非流式版本)- 支持多模型、图片输入、JSON输出和模板提示词
|
|
* 通用文生文方法(非流式版本)- 支持多模型、图片输入、JSON输出和模板提示词
|
|
@@ -1193,6 +1219,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
private function deepSeekStreamResponse($post_data) {
|
|
private function deepSeekStreamResponse($post_data) {
|
|
|
// 设置最大输出tokens
|
|
// 设置最大输出tokens
|
|
|
$post_data['max_tokens'] = 300000;
|
|
$post_data['max_tokens'] = 300000;
|
|
|
|
|
+ // 流式请求显式声明返回 usage(在最后一个 chunk 中返回)
|
|
|
|
|
+ $post_data['stream_options'] = ['include_usage' => true];
|
|
|
|
|
|
|
|
$client = new Client(['timeout' => 1800, 'verify' => false]);
|
|
$client = new Client(['timeout' => 1800, 'verify' => false]);
|
|
|
$response = $client->post($this->url, [
|
|
$response = $client->post($this->url, [
|
|
@@ -1324,6 +1352,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
unset($post_data['reasoning_effort']);
|
|
unset($post_data['reasoning_effort']);
|
|
|
}
|
|
}
|
|
|
$post_data['max_completion_tokens'] = 100000;
|
|
$post_data['max_completion_tokens'] = 100000;
|
|
|
|
|
+ // 流式请求显式声明返回 usage(在最后一个 chunk 中返回)
|
|
|
|
|
+ $post_data['stream_options'] = ['include_usage' => true];
|
|
|
|
|
|
|
|
// 备用中转站地址1: https://token.ithinkai.cn/v1/chat/completions
|
|
// 备用中转站地址1: https://token.ithinkai.cn/v1/chat/completions
|
|
|
// 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
|
|
// 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
|
|
@@ -5866,6 +5896,11 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$prompt = getProp($data, 'prompt');
|
|
$prompt = getProp($data, 'prompt');
|
|
|
$is_multi = getProp($anime, 'is_multi');
|
|
$is_multi = getProp($anime, 'is_multi');
|
|
|
$is_single = (int)$is_multi !== 1;
|
|
$is_single = (int)$is_multi !== 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 积分余额预检(仅单剧集模式收费)
|
|
|
|
|
+ if ($is_single) {
|
|
|
|
|
+ $this->pointsService->checkUserPointsEnough(PointsService::CHAT_CHARGE_POINTS, $uid);
|
|
|
|
|
+ }
|
|
|
$extra_products = getProp($data, 'products', []);
|
|
$extra_products = getProp($data, 'products', []);
|
|
|
if (!$extra_products) {
|
|
if (!$extra_products) {
|
|
|
$extra_products = getProp($anime, 'extra_products');
|
|
$extra_products = getProp($anime, 'extra_products');
|
|
@@ -6728,6 +6763,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
Utils::throwError('20003:单剧集分镜记录保存失败');
|
|
Utils::throwError('20003:单剧集分镜记录保存失败');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 单剧集模式调用成功,返回结果前扣除积分并记录明细(与保存同事务)
|
|
|
|
|
+ if ($is_single && !empty($single_episode)) {
|
|
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
|
|
+ 'anime_id' => $anime_id,
|
|
|
|
|
+ 'episode_id' => $episode_id ?? 0,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
}catch (\Exception $e) {
|
|
}catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
DB::rollBack();
|
|
|
dLog('deepseek')->info('新建对话错误信息: '.$e->getMessage());
|
|
dLog('deepseek')->info('新建对话错误信息: '.$e->getMessage());
|
|
@@ -6785,6 +6828,11 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
}
|
|
|
$is_multi = getProp($anime, 'is_multi', 1);
|
|
$is_multi = getProp($anime, 'is_multi', 1);
|
|
|
$is_single = (int)$is_multi !== 1;
|
|
$is_single = (int)$is_multi !== 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 积分余额预检(仅单剧集模式收费)
|
|
|
|
|
+ if ($is_single) {
|
|
|
|
|
+ $this->pointsService->checkUserPointsEnough(PointsService::CHAT_CHARGE_POINTS, $uid);
|
|
|
|
|
+ }
|
|
|
$extra_products = getProp($data, 'products', []);
|
|
$extra_products = getProp($data, 'products', []);
|
|
|
if (!$extra_products) {
|
|
if (!$extra_products) {
|
|
|
$extra_products = getProp($anime, 'extra_products');
|
|
$extra_products = getProp($anime, 'extra_products');
|
|
@@ -7697,6 +7745,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
Utils::throwError('20003:单剧集分镜记录保存失败');
|
|
Utils::throwError('20003:单剧集分镜记录保存失败');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 单剧集模式调用成功,返回结果前扣除积分并记录明细(与保存同事务)
|
|
|
|
|
+ if ($is_single && !empty($single_episode)) {
|
|
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
|
|
+ 'anime_id' => $anime_id,
|
|
|
|
|
+ 'episode_id' => $episode_id ?? 0,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
}catch (\Exception $e) {
|
|
}catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
DB::rollBack();
|
|
@@ -7743,6 +7799,10 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
|
|
|
|
|
public function chatForAce($data) {
|
|
public function chatForAce($data) {
|
|
|
$uid = Site::getUid();
|
|
$uid = Site::getUid();
|
|
|
|
|
+
|
|
|
|
|
+ // 积分余额预检(不足直接报错)
|
|
|
|
|
+ $this->pointsService->checkUserPointsEnough(PointsService::CHAT_CHARGE_POINTS, $uid);
|
|
|
|
|
+
|
|
|
$anime_id = getProp($data, 'anime_id');
|
|
$anime_id = getProp($data, 'anime_id');
|
|
|
$file = getProp($data, 'file');
|
|
$file = getProp($data, 'file');
|
|
|
$content = getProp($data, 'content', '');
|
|
$content = getProp($data, 'content', '');
|
|
@@ -8877,6 +8937,13 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$update_anime_data['content'] = $uploaded_content;
|
|
$update_anime_data['content'] = $uploaded_content;
|
|
|
}
|
|
}
|
|
|
DB::table('mp_animes')->where('id', $anime_id)->update($update_anime_data);
|
|
DB::table('mp_animes')->where('id', $anime_id)->update($update_anime_data);
|
|
|
|
|
+
|
|
|
|
|
+ // 调用成功,返回结果前扣除积分并记录明细(与保存同事务)
|
|
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
|
|
+ 'anime_id' => $anime_id,
|
|
|
|
|
+ 'episode_number' => $episode_number,
|
|
|
|
|
+ 'model' => $model,
|
|
|
|
|
+ ]);
|
|
|
}catch (\Exception $e) {
|
|
}catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
DB::rollBack();
|
|
|
dLog('deepseek')->info('新建对话错误信息: '.$e->getMessage());
|
|
dLog('deepseek')->info('新建对话错误信息: '.$e->getMessage());
|
|
@@ -9043,7 +9110,22 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
if ($end_episode_sequence && $endEpisodeNumber > $end_episode_sequence) {
|
|
if ($end_episode_sequence && $endEpisodeNumber > $end_episode_sequence) {
|
|
|
Utils::throwError("20003:总集数只有{$end_episode_sequence}集,无法生成到第{$endEpisodeNumber}集");
|
|
Utils::throwError("20003:总集数只有{$end_episode_sequence}集,无法生成到第{$endEpisodeNumber}集");
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 计算本次实际需要创建的任务数(跳过已存在且未失败的任务;失败任务会删除重建,算作创建)
|
|
|
|
|
+ $existingSkippedCount = DB::table('mp_batch_episode_generation_details')
|
|
|
|
|
+ ->where('anime_id', $anime_id)
|
|
|
|
|
+ ->whereBetween('episode_number', [$startEpisodeNumber, $endEpisodeNumber])
|
|
|
|
|
+ ->whereIn('status', ['pending', 'processing', 'completed'])
|
|
|
|
|
+ ->count();
|
|
|
|
|
+
|
|
|
|
|
+ $needEpisodeCount = ($endEpisodeNumber - $startEpisodeNumber + 1) - $existingSkippedCount;
|
|
|
|
|
+
|
|
|
|
|
+ // 积分余额预检:每个剧集生成成功将扣 CHAT_CHARGE_POINTS 积分。
|
|
|
|
|
+ // 此处仅预检不扣费,实际扣费由定时任务调用 chatForAce(经 chatForAceNonStream)时按集扣取,避免重复扣费。
|
|
|
|
|
+ if ($needEpisodeCount > 0) {
|
|
|
|
|
+ $this->pointsService->checkUserPointsEnough(PointsService::CHAT_CHARGE_POINTS * $needEpisodeCount, $uid);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
$now = date('Y-m-d H:i:s');
|
|
$now = date('Y-m-d H:i:s');
|
|
|
$createdTasks = [];
|
|
$createdTasks = [];
|
|
|
$skippedTasks = [];
|
|
$skippedTasks = [];
|
|
@@ -9412,6 +9494,11 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
if (!empty($act_id) && !empty($episode_id)) {
|
|
if (!empty($act_id) && !empty($episode_id)) {
|
|
|
Utils::throwError('20003:片段ID和剧集ID只能选择一个');
|
|
Utils::throwError('20003:片段ID和剧集ID只能选择一个');
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 积分余额预检(仅剧集ID模式收费)
|
|
|
|
|
+ if (!empty($episode_id)) {
|
|
|
|
|
+ $this->pointsService->checkUserPointsEnough(PointsService::CHAT_CHARGE_POINTS, $uid);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 如果提供了template_id,获取模板提示词
|
|
// 如果提供了template_id,获取模板提示词
|
|
|
$template_prompt = '';
|
|
$template_prompt = '';
|
|
@@ -9992,6 +10079,12 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
if (!empty($segments_to_insert)) {
|
|
if (!empty($segments_to_insert)) {
|
|
|
DB::table('mp_episode_segments')->insert($segments_to_insert);
|
|
DB::table('mp_episode_segments')->insert($segments_to_insert);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 剧集ID模式调用成功,返回结果前扣除积分并记录明细(与保存同事务)
|
|
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
|
|
+ 'anime_id' => $target_anime_id,
|
|
|
|
|
+ 'episode_id' => $target_episode_id,
|
|
|
|
|
+ ]);
|
|
|
|
|
|
|
|
DB::commit();
|
|
DB::commit();
|
|
|
|
|
|
|
@@ -12406,9 +12499,11 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
try {
|
|
try {
|
|
|
// 判断是否为流式输出
|
|
// 判断是否为流式输出
|
|
|
$isStream = isset($params['stream']) && $params['stream'] === true;
|
|
$isStream = isset($params['stream']) && $params['stream'] === true;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if ($isStream) {
|
|
if ($isStream) {
|
|
|
// 流式输出
|
|
// 流式输出
|
|
|
|
|
+ // 流式请求默认声明返回 usage(未显式传参时)
|
|
|
|
|
+ $requestData['stream_options'] = $requestData['stream_options'] ?? ['include_usage' => true];
|
|
|
return $this->volcEngineChatCompletionStream($client, $apiKey, $requestData);
|
|
return $this->volcEngineChatCompletionStream($client, $apiKey, $requestData);
|
|
|
} else {
|
|
} else {
|
|
|
// 非流式输出
|
|
// 非流式输出
|