|
|
@@ -9,6 +9,7 @@ use App\Libs\Utils;
|
|
|
use GuzzleHttp\Client;
|
|
|
use DateTime;
|
|
|
use DateTimeZone;
|
|
|
+use App\Models\MpUserPointsDetail;
|
|
|
use App\Services\AIGeneration\AIImageGenerationService;
|
|
|
use App\Services\PointsService;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
@@ -332,7 +333,32 @@ class DeepSeekService
|
|
|
if ($chargePoints > 0) {
|
|
|
return $this->wrapGenerateTextCharge($streamGenerator, $uid, $model, $chargePoints, $chargeModality);
|
|
|
}
|
|
|
- return $streamGenerator;
|
|
|
+
|
|
|
+ // 纯文本(不扣积分):流式生成成功后仅记录 token 使用明细
|
|
|
+ return $this->wrapGenerateTextTokenRecord($streamGenerator, $uid, $model);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 流式生成成功后仅记录 token 明细(纯文本免费场景,不扣积分)
|
|
|
+ *
|
|
|
+ * @param \Generator $generator
|
|
|
+ * @param int $uid
|
|
|
+ * @param string $model
|
|
|
+ * @return \Generator
|
|
|
+ */
|
|
|
+ private function wrapGenerateTextTokenRecord(\Generator $generator, int $uid, string $model): \Generator
|
|
|
+ {
|
|
|
+ $recorded = false;
|
|
|
+ foreach ($generator as $chunk) {
|
|
|
+ if (!$recorded && isset($chunk['type']) && $chunk['type'] === 'done') {
|
|
|
+ $this->recordTextTokenOnly($uid, $this->pointsService->getTokensFromUsage(getProp($chunk, 'usage', [])), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'generateText',
|
|
|
+ ], $model);
|
|
|
+ $recorded = true;
|
|
|
+ }
|
|
|
+ yield $chunk;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -509,6 +535,53 @@ class DeepSeekService
|
|
|
throw new \Exception('AI对话计费失败: ' . (string)($result['reason'] ?? 'unknown'));
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全获取当前登录用户ID
|
|
|
+ *
|
|
|
+ * 命令行/队列等无用户上下文的场景返回 0,由记录方法自行跳过。
|
|
|
+ *
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ private function getCurrentUid(): int
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ return (int)Site::getUid();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录文生文 token 使用明细(仅记录,不扣积分)
|
|
|
+ *
|
|
|
+ * 用于不按积分计费的文生文接口;记录失败不影响主流程。
|
|
|
+ *
|
|
|
+ * @param int $uid 用户ID(0 时跳过)
|
|
|
+ * @param int $tokens 消耗 token 数
|
|
|
+ * @param array $chargeInfo 计费/来源信息(model、source 等)
|
|
|
+ * @param string $apiType API 类型/模型标识
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ private function recordTextTokenOnly(int $uid, int $tokens, array $chargeInfo, string $apiType = ''): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $this->pointsService->recordTokenOnlyDetail(
|
|
|
+ $uid,
|
|
|
+ $tokens,
|
|
|
+ $chargeInfo,
|
|
|
+ MpUserPointsDetail::TYPE_CHAT,
|
|
|
+ $apiType,
|
|
|
+ ''
|
|
|
+ );
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ dLog('points')->warning('token明细记录异常(不影响主流程): ' . $e->getMessage(), [
|
|
|
+ 'uid' => $uid,
|
|
|
+ 'source' => $chargeInfo['source'] ?? '',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 拆分剧本内容并校验章节总数与标题连续性
|
|
|
*
|
|
|
@@ -680,6 +753,12 @@ class DeepSeekService
|
|
|
if (preg_match('/"invalid_index"\s*:\s*(\d+)/i', $content, $match)) {
|
|
|
$result['invalid_index'] = (int)$match[1];
|
|
|
}
|
|
|
+
|
|
|
+ // 仅记录 token 使用明细(不扣积分):该校验为流程内的额外模型调用
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($result['usage'] ?? []), [
|
|
|
+ 'model' => 'deepseek-v4-flash',
|
|
|
+ 'source' => 'validateChapterSequenceByAI',
|
|
|
+ ], 'deepseek-v4-flash');
|
|
|
} catch (\Exception $e) {
|
|
|
dLog('deepseek')->error('章节连续性AI校验失败: ' . $e->getMessage());
|
|
|
}
|
|
|
@@ -3028,6 +3107,12 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
'words' => getProp($script_content, 'words'),
|
|
|
];
|
|
|
|
|
|
+ // 仅记录 token 使用明细(不扣积分)
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'chatWithReasoner',
|
|
|
+ ], $model);
|
|
|
+
|
|
|
return $result;
|
|
|
}
|
|
|
|
|
|
@@ -4116,6 +4201,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
|
DB::commit();
|
|
|
|
|
|
+ // 仅记录 token 使用明细(不扣积分)
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'group_id' => $group_id,
|
|
|
+ 'source' => 'generateEpisodes',
|
|
|
+ ], $model);
|
|
|
+
|
|
|
yield [
|
|
|
'type' => 'done',
|
|
|
'script' => $script_arr,
|
|
|
@@ -4407,6 +4500,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
|
DB::commit();
|
|
|
|
|
|
+ // 仅记录 token 使用明细(不扣积分)
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'group_id' => $group_id,
|
|
|
+ 'source' => 'chatWithFileStream',
|
|
|
+ ], $model);
|
|
|
+
|
|
|
yield [
|
|
|
'type' => 'done',
|
|
|
'script' => $script_arr,
|
|
|
@@ -4546,6 +4647,13 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
|
]);
|
|
|
|
|
|
+ // 仅记录 token 使用明细(不扣积分)
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'source' => 'chatWithFile',
|
|
|
+ ], $model);
|
|
|
+
|
|
|
return [
|
|
|
'script' => $script_arr,
|
|
|
'answer' => $fullContent,
|
|
|
@@ -5799,6 +5907,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $anime_id,
|
|
|
'episode_id' => $episode_id ?? 0,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'addChat',
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
@@ -6611,6 +6721,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $anime_id,
|
|
|
'episode_id' => $episode_id ?? 0,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'reGenerateAnime',
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
@@ -7383,6 +7495,7 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
'anime_id' => $anime_id,
|
|
|
'episode_number' => $episode_number,
|
|
|
'model' => $model,
|
|
|
+ 'source' => 'chat',
|
|
|
]);
|
|
|
}catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
|
@@ -8317,6 +8430,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $anime_id,
|
|
|
'episode_id' => $episode_id ?? 0,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'addChatForAce',
|
|
|
]);
|
|
|
}
|
|
|
}catch (\Exception $e) {
|
|
|
@@ -9303,6 +9418,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $anime_id,
|
|
|
'episode_id' => $episode_id ?? 0,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'reGenerateAnimeForAce',
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
@@ -10807,6 +10924,7 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
'anime_id' => $anime_id,
|
|
|
'episode_number' => $episode_number,
|
|
|
'model' => $model,
|
|
|
+ 'source' => 'chatForAce',
|
|
|
]);
|
|
|
}catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
|
@@ -11993,6 +12111,8 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $target_anime_id,
|
|
|
'episode_id' => $target_episode_id,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'regenerateSegmentScript',
|
|
|
]);
|
|
|
|
|
|
DB::commit();
|
|
|
@@ -13716,6 +13836,12 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
Utils::throwError('20003: 接口调用失败!');
|
|
|
}
|
|
|
|
|
|
+ // 仅记录 token 使用明细(不扣积分)
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'chatChangeImg',
|
|
|
+ ], $model);
|
|
|
+
|
|
|
return [
|
|
|
'type' => 'done',
|
|
|
// 'episode' => $episode,
|
|
|
@@ -14289,6 +14415,7 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
|
|
|
// 根据模型类型选择调用方法
|
|
|
$fullContent = '';
|
|
|
+ $usage = [];
|
|
|
|
|
|
if (in_array($model, ['deepseek-reasoner', 'deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'])) {
|
|
|
// DeepSeek 官方模型使用 DeepSeek API
|
|
|
@@ -14303,8 +14430,16 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
|
|
|
if (is_array($chatResult)) {
|
|
|
$fullContent = $chatResult['fullContent'];
|
|
|
+ $usage = $chatResult['usage'] ?? [];
|
|
|
}
|
|
|
|
|
|
+ // 仅记录 token 使用明细(不扣积分)
|
|
|
+ $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'cid' => $cid,
|
|
|
+ 'source' => 'generateScriptWords',
|
|
|
+ ], $model);
|
|
|
+
|
|
|
return $fullContent;
|
|
|
}
|
|
|
|