|
|
@@ -25,6 +25,7 @@ class DeepSeekService
|
|
|
private $api_key;
|
|
|
private $sys_message;
|
|
|
private $headers;
|
|
|
+ private $valid_text_models;
|
|
|
protected $aiImageGenerationService;
|
|
|
|
|
|
public function __construct(AIImageGenerationService $aiImageGenerationService) {
|
|
|
@@ -35,6 +36,7 @@ class DeepSeekService
|
|
|
'Authorization' => 'Bearer '.$this->api_key,
|
|
|
'Content-Type' => 'application/json; charset=UTF-8'
|
|
|
];
|
|
|
+ $this->valid_text_models = ['doubao-seed-2-0-pro-260215', 'doubao-seed-2-0-lite-260215', 'deepseek-v3-2-251201'];
|
|
|
$this->sys_message = [
|
|
|
'role' => 'system',
|
|
|
'content' => "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容完成示例格式的内容输出(需通过以下几个板块进行回复: <故事梗概><剧本亮点><人物关系><核心矛盾><主体列表><美术风格><场景列表><分集剧本>,每个板块之间用###分隔;同时板块之间需满足以下要求:\n
|
|
|
@@ -141,7 +143,7 @@ class DeepSeekService
|
|
|
{
|
|
|
return [
|
|
|
'日系动漫风格' => [
|
|
|
- 'aliases' => ['日系动漫风格', '日系动漫', '日系', '动漫'],
|
|
|
+ 'aliases' => ['日系动漫风格', '日系动漫', '日系', '动漫', '日漫'],
|
|
|
'prompt' => "日系动漫风格,线条干净流畅,色彩明亮柔和,人物精致,光影细腻,画面统一稳定"
|
|
|
],
|
|
|
'国漫风格' => [
|
|
|
@@ -244,6 +246,116 @@ class DeepSeekService
|
|
|
return preg_replace($pattern, "###美术风格\n" . trim($artStyle), $content, 1);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * DeepSeek流式输出处理方法
|
|
|
+ *
|
|
|
+ * @param array $post_data DeepSeek API请求参数
|
|
|
+ * @return \Generator 返回生成器,用于流式输出
|
|
|
+ */
|
|
|
+ private function deepSeekStreamResponse($post_data) {
|
|
|
+ $client = new Client(['timeout' => 1200, 'verify' => false]);
|
|
|
+ $response = $client->post($this->url, [
|
|
|
+ 'json' => $post_data,
|
|
|
+ 'headers' => $this->headers,
|
|
|
+ 'stream' => true // 启用流式响应
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $body = $response->getBody();
|
|
|
+ $fullContent = '';
|
|
|
+ $fullReasoningContent = '';
|
|
|
+ $usage = [];
|
|
|
+ $buffer = '';
|
|
|
+
|
|
|
+ dLog('deepseek')->info('开始读取 DeepSeek 流式响应');
|
|
|
+
|
|
|
+ // 逐行读取流式响应
|
|
|
+ while (!$body->eof()) {
|
|
|
+ $chunk = $body->read(1024); // 每次读取 1KB
|
|
|
+ $buffer .= $chunk;
|
|
|
+
|
|
|
+ // 按行分割
|
|
|
+ $lines = explode("\n", $buffer);
|
|
|
+ // 保留最后一个不完整的行
|
|
|
+ $buffer = array_pop($lines);
|
|
|
+
|
|
|
+ foreach ($lines as $line) {
|
|
|
+ $line = trim($line);
|
|
|
+
|
|
|
+ // 跳过空行
|
|
|
+ if (empty($line)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是 SSE 数据行
|
|
|
+ if (strpos($line, 'data: ') !== 0) {
|
|
|
+ dLog('deepseek')->warning('非 SSE 数据行', ['line' => $line]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $data = substr($line, 6); // 移除 "data: " 前缀
|
|
|
+
|
|
|
+ if ($data === '[DONE]') {
|
|
|
+ dLog('deepseek')->info('收到结束标记');
|
|
|
+ break 2; // 跳出两层循环
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $json = json_decode($data, true);
|
|
|
+
|
|
|
+ if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
+ dLog('deepseek')->warning('JSON 解析错误: ' . json_last_error_msg(), ['data' => $data]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($json['choices'][0]['delta'])) {
|
|
|
+ $delta = $json['choices'][0]['delta'];
|
|
|
+
|
|
|
+ // 处理思维链内容(仅 deepseek-reasoner 模型)
|
|
|
+ if (isset($delta['reasoning_content'])) {
|
|
|
+ $fullReasoningContent .= $delta['reasoning_content'];
|
|
|
+ yield [
|
|
|
+ 'type' => 'reasoning',
|
|
|
+ 'content' => $delta['reasoning_content'],
|
|
|
+ 'full_reasoning' => $fullReasoningContent
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理最终回答内容
|
|
|
+ if (isset($delta['content'])) {
|
|
|
+ $fullContent .= $delta['content'];
|
|
|
+ yield [
|
|
|
+ 'type' => 'content',
|
|
|
+ 'content' => $delta['content'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取使用统计信息
|
|
|
+ if (isset($json['usage'])) {
|
|
|
+ $usage = $json['usage'];
|
|
|
+ dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->info('流式读取完成', [
|
|
|
+ 'content_length' => strlen($fullContent),
|
|
|
+ 'reasoning_length' => strlen($fullReasoningContent)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ yield [
|
|
|
+ 'type' => 'done',
|
|
|
+ 'full_content' => $fullContent,
|
|
|
+ 'full_reasoning' => $fullReasoningContent,
|
|
|
+ 'usage' => $usage
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
// 与推理模型对话
|
|
|
public function chatWithReasoner($data) {
|
|
|
$content = getProp($data, 'content');
|
|
|
@@ -1919,108 +2031,34 @@ class DeepSeekService
|
|
|
'stream' => true // 启用流式输出
|
|
|
];
|
|
|
|
|
|
- $client = new Client(['timeout' => 1200, 'verify' => false]);
|
|
|
- $response = $client->post($this->url, [
|
|
|
- 'json' => $post_data,
|
|
|
- 'headers' => $this->headers,
|
|
|
- 'stream' => true // 启用流式响应
|
|
|
- ]);
|
|
|
-
|
|
|
- $body = $response->getBody();
|
|
|
+ // 根据模型类型选择调用方法
|
|
|
$fullContent = '';
|
|
|
$fullReasoningContent = '';
|
|
|
$usage = [];
|
|
|
- $buffer = '';
|
|
|
-
|
|
|
- dLog('deepseek')->info('开始读取 DeepSeek 流式响应');
|
|
|
-
|
|
|
- // 逐行读取流式响应
|
|
|
- while (!$body->eof()) {
|
|
|
- $chunk = $body->read(1024); // 每次读取 1KB
|
|
|
- $buffer .= $chunk;
|
|
|
-
|
|
|
- // 按行分割
|
|
|
- $lines = explode("\n", $buffer);
|
|
|
- // 保留最后一个不完整的行
|
|
|
- $buffer = array_pop($lines);
|
|
|
-
|
|
|
- foreach ($lines as $line) {
|
|
|
- $line = trim($line);
|
|
|
-
|
|
|
- // 跳过空行
|
|
|
- if (empty($line)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // dLog('deepseek')->info('收到原始行数据', ['line' => $line]);
|
|
|
-
|
|
|
- // 检查是否是 SSE 数据行
|
|
|
- if (strpos($line, 'data: ') !== 0) {
|
|
|
- dLog('deepseek')->warning('非 SSE 数据行', ['line' => $line]);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- $data = substr($line, 6); // 移除 "data: " 前缀
|
|
|
-
|
|
|
- if ($data === '[DONE]') {
|
|
|
- dLog('deepseek')->info('收到结束标记');
|
|
|
- break 2; // 跳出两层循环
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- $json = json_decode($data, true);
|
|
|
-
|
|
|
- if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
- dLog('deepseek')->warning('JSON 解析错误: ' . json_last_error_msg(), ['data' => $data]);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // dLog('deepseek')->info('成功解析 JSON', ['has_choices' => isset($json['choices'])]);
|
|
|
-
|
|
|
- if (isset($json['choices'][0]['delta'])) {
|
|
|
- $delta = $json['choices'][0]['delta'];
|
|
|
-
|
|
|
- // 处理思维链内容(仅 deepseek-reasoner 模型)
|
|
|
- // 注释掉以下内容则不返回d思维链内容
|
|
|
- if (isset($delta['reasoning_content'])) {
|
|
|
- $fullReasoningContent .= $delta['reasoning_content'];
|
|
|
- // dLog('deepseek')->info('输出思维链内容', ['length' => strlen($delta['reasoning_content'])]);
|
|
|
- yield [
|
|
|
- 'type' => 'reasoning',
|
|
|
- 'content' => $delta['reasoning_content'],
|
|
|
- 'full_reasoning' => $fullReasoningContent
|
|
|
- ];
|
|
|
- }
|
|
|
-
|
|
|
- // 处理最终回答内容
|
|
|
- if (isset($delta['content'])) {
|
|
|
- $fullContent .= $delta['content'];
|
|
|
- // dLog('deepseek')->info('输出回答内容', ['length' => strlen($delta['content'])]);
|
|
|
- yield [
|
|
|
- 'type' => 'content',
|
|
|
- 'content' => $delta['content'],
|
|
|
- // 'full_content' => $fullContent
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 获取使用统计信息
|
|
|
- if (isset($json['usage'])) {
|
|
|
- $usage = $json['usage'];
|
|
|
- dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
|
|
|
- }
|
|
|
-
|
|
|
- } catch (\Exception $e) {
|
|
|
- dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
|
|
|
- continue;
|
|
|
+
|
|
|
+ if (in_array($model, ['deepseek-reasoner', 'deepseek-chat'])) {
|
|
|
+ // R1模型使用DeepSeek API
|
|
|
+ $streamGenerator = $this->deepSeekStreamResponse($post_data);
|
|
|
+ } else {
|
|
|
+ // V3模型使用火山引擎API
|
|
|
+ $streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理流式输出
|
|
|
+ foreach ($streamGenerator as $chunk) {
|
|
|
+ if (isset($chunk['type'])) {
|
|
|
+ if ($chunk['type'] === 'done') {
|
|
|
+ // 最终结果
|
|
|
+ $fullContent = $chunk['full_content'];
|
|
|
+ $fullReasoningContent = $chunk['full_reasoning'];
|
|
|
+ $usage = isset($chunk['usage']) ? $chunk['usage'] : [];
|
|
|
+ } else {
|
|
|
+ // 逐块yield数据
|
|
|
+ yield $chunk;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- dLog('deepseek')->info('流式读取完成', [
|
|
|
- 'content_length' => strlen($fullContent),
|
|
|
- 'reasoning_length' => strlen($fullReasoningContent)
|
|
|
- ]);
|
|
|
dLog('deepseek')->info('完整内容: '.$fullContent);
|
|
|
|
|
|
// 处理完整内容并返回最终结果
|
|
|
@@ -2344,108 +2382,34 @@ class DeepSeekService
|
|
|
'stream' => true // 启用流式输出
|
|
|
];
|
|
|
|
|
|
- $client = new Client(['timeout' => 1200, 'verify' => false]);
|
|
|
- $response = $client->post($this->url, [
|
|
|
- 'json' => $post_data,
|
|
|
- 'headers' => $this->headers,
|
|
|
- 'stream' => true // 启用流式响应
|
|
|
- ]);
|
|
|
-
|
|
|
- $body = $response->getBody();
|
|
|
+ // 根据模型类型选择调用方法
|
|
|
$fullContent = '';
|
|
|
$fullReasoningContent = '';
|
|
|
$usage = [];
|
|
|
- $buffer = '';
|
|
|
-
|
|
|
- dLog('deepseek')->info('开始读取 DeepSeek 流式响应');
|
|
|
-
|
|
|
- // 逐行读取流式响应
|
|
|
- while (!$body->eof()) {
|
|
|
- $chunk = $body->read(1024); // 每次读取 1KB
|
|
|
- $buffer .= $chunk;
|
|
|
-
|
|
|
- // 按行分割
|
|
|
- $lines = explode("\n", $buffer);
|
|
|
- // 保留最后一个不完整的行
|
|
|
- $buffer = array_pop($lines);
|
|
|
-
|
|
|
- foreach ($lines as $line) {
|
|
|
- $line = trim($line);
|
|
|
-
|
|
|
- // 跳过空行
|
|
|
- if (empty($line)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // dLog('deepseek')->info('收到原始行数据', ['line' => $line]);
|
|
|
-
|
|
|
- // 检查是否是 SSE 数据行
|
|
|
- if (strpos($line, 'data: ') !== 0) {
|
|
|
- dLog('deepseek')->warning('非 SSE 数据行', ['line' => $line]);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- $data = substr($line, 6); // 移除 "data: " 前缀
|
|
|
-
|
|
|
- if ($data === '[DONE]') {
|
|
|
- dLog('deepseek')->info('收到结束标记');
|
|
|
- break 2; // 跳出两层循环
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- $json = json_decode($data, true);
|
|
|
-
|
|
|
- if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
- dLog('deepseek')->warning('JSON 解析错误: ' . json_last_error_msg(), ['data' => $data]);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // dLog('deepseek')->info('成功解析 JSON', ['has_choices' => isset($json['choices'])]);
|
|
|
-
|
|
|
- if (isset($json['choices'][0]['delta'])) {
|
|
|
- $delta = $json['choices'][0]['delta'];
|
|
|
-
|
|
|
- // 处理思维链内容(仅 deepseek-reasoner 模型)
|
|
|
- // 注释掉以下内容则不返回d思维链内容
|
|
|
- if (isset($delta['reasoning_content'])) {
|
|
|
- $fullReasoningContent .= $delta['reasoning_content'];
|
|
|
- // dLog('deepseek')->info('输出思维链内容', ['length' => strlen($delta['reasoning_content'])]);
|
|
|
- yield [
|
|
|
- 'type' => 'reasoning',
|
|
|
- 'content' => $delta['reasoning_content'],
|
|
|
- 'full_reasoning' => $fullReasoningContent
|
|
|
- ];
|
|
|
- }
|
|
|
-
|
|
|
- // 处理最终回答内容
|
|
|
- if (isset($delta['content'])) {
|
|
|
- $fullContent .= $delta['content'];
|
|
|
- // dLog('deepseek')->info('输出回答内容', ['length' => strlen($delta['content'])]);
|
|
|
- yield [
|
|
|
- 'type' => 'content',
|
|
|
- 'content' => $delta['content'],
|
|
|
- // 'full_content' => $fullContent
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 获取使用统计信息
|
|
|
- if (isset($json['usage'])) {
|
|
|
- $usage = $json['usage'];
|
|
|
- dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
|
|
|
- }
|
|
|
-
|
|
|
- } catch (\Exception $e) {
|
|
|
- dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
|
|
|
- continue;
|
|
|
+
|
|
|
+ if (in_array($model, ['deepseek-reasoner', 'deepseek-chat'])) {
|
|
|
+ // R1模型使用DeepSeek API
|
|
|
+ $streamGenerator = $this->deepSeekStreamResponse($post_data);
|
|
|
+ } else {
|
|
|
+ // V3模型使用火山引擎API
|
|
|
+ $streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理流式输出
|
|
|
+ foreach ($streamGenerator as $chunk) {
|
|
|
+ if (isset($chunk['type'])) {
|
|
|
+ if ($chunk['type'] === 'done') {
|
|
|
+ // 最终结果
|
|
|
+ $fullContent = $chunk['full_content'];
|
|
|
+ $fullReasoningContent = $chunk['full_reasoning'];
|
|
|
+ $usage = isset($chunk['usage']) ? $chunk['usage'] : [];
|
|
|
+ } else {
|
|
|
+ // 逐块yield数据
|
|
|
+ yield $chunk;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- dLog('deepseek')->info('流式读取完成', [
|
|
|
- 'content_length' => strlen($fullContent),
|
|
|
- 'reasoning_length' => strlen($fullReasoningContent)
|
|
|
- ]);
|
|
|
dLog('deepseek')->info('完整内容: '.$fullContent);
|
|
|
|
|
|
// 处理完整内容并返回最终结果
|
|
|
@@ -2776,109 +2740,34 @@ class DeepSeekService
|
|
|
'stream' => true // 启用流式输出
|
|
|
];
|
|
|
|
|
|
- $client = new Client(['timeout' => 1200, 'verify' => false]);
|
|
|
- $response = $client->post($this->url, [
|
|
|
- 'json' => $post_data,
|
|
|
- 'headers' => $this->headers,
|
|
|
- 'stream' => true // 启用流式响应
|
|
|
- ]);
|
|
|
-
|
|
|
- $body = $response->getBody();
|
|
|
+ // 根据模型类型选择调用方法
|
|
|
$fullContent = '';
|
|
|
$fullReasoningContent = '';
|
|
|
$usage = [];
|
|
|
- $buffer = '';
|
|
|
-
|
|
|
- dLog('deepseek')->info('开始读取 DeepSeek 流式响应');
|
|
|
-
|
|
|
- // 逐行读取流式响应
|
|
|
- while (!$body->eof()) {
|
|
|
- $chunk = $body->read(1024); // 每次读取 1KB
|
|
|
- $buffer .= $chunk;
|
|
|
-
|
|
|
- // 按行分割
|
|
|
- $lines = explode("\n", $buffer);
|
|
|
- // 保留最后一个不完整的行
|
|
|
- $buffer = array_pop($lines);
|
|
|
-
|
|
|
- foreach ($lines as $line) {
|
|
|
- $line = trim($line);
|
|
|
-
|
|
|
- // 跳过空行
|
|
|
- if (empty($line)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // dLog('deepseek')->info('收到原始行数据', ['line' => $line]);
|
|
|
-
|
|
|
- // 检查是否是 SSE 数据行
|
|
|
- if (strpos($line, 'data: ') !== 0) {
|
|
|
- dLog('deepseek')->warning('非 SSE 数据行', ['line' => $line]);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- $data = substr($line, 6); // 移除 "data: " 前缀
|
|
|
-
|
|
|
- if ($data === '[DONE]') {
|
|
|
- dLog('deepseek')->info('收到结束标记');
|
|
|
- break 2; // 跳出两层循环
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- $json = json_decode($data, true);
|
|
|
-
|
|
|
- if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
- dLog('deepseek')->warning('JSON 解析错误: ' . json_last_error_msg(), ['data' => $data]);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // dLog('deepseek')->info('成功解析 JSON', ['has_choices' => isset($json['choices'])]);
|
|
|
-
|
|
|
- if (isset($json['choices'][0]['delta'])) {
|
|
|
- $delta = $json['choices'][0]['delta'];
|
|
|
-
|
|
|
- // 处理思维链内容(仅 deepseek-reasoner 模型)
|
|
|
- // 注释掉以下内容则不返回d思维链内容
|
|
|
- if (isset($delta['reasoning_content'])) {
|
|
|
- $fullReasoningContent .= $delta['reasoning_content'];
|
|
|
- // dLog('deepseek')->info('输出思维链内容', ['length' => strlen($delta['reasoning_content'])]);
|
|
|
- yield [
|
|
|
- 'type' => 'reasoning',
|
|
|
- 'content' => $delta['reasoning_content'],
|
|
|
- 'full_reasoning' => $fullReasoningContent
|
|
|
- ];
|
|
|
- }
|
|
|
-
|
|
|
- // 处理最终回答内容
|
|
|
- if (isset($delta['content'])) {
|
|
|
- $fullContent .= $delta['content'];
|
|
|
- // dLog('deepseek')->info('输出回答内容', ['length' => strlen($delta['content'])]);
|
|
|
- yield [
|
|
|
- 'type' => 'content',
|
|
|
- 'content' => $delta['content'],
|
|
|
- // 'full_content' => $fullContent
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 获取使用统计信息
|
|
|
- if (isset($json['usage'])) {
|
|
|
- $usage = $json['usage'];
|
|
|
- dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
|
|
|
- }
|
|
|
-
|
|
|
- } catch (\Exception $e) {
|
|
|
- dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
|
|
|
- continue;
|
|
|
+
|
|
|
+ if (in_array($model, ['deepseek-reasoner', 'deepseek-chat'])) {
|
|
|
+ // R1模型使用DeepSeek API
|
|
|
+ $streamGenerator = $this->deepSeekStreamResponse($post_data);
|
|
|
+ } else {
|
|
|
+ // V3模型使用火山引擎API
|
|
|
+ $streamGenerator = $this->volcEngineChatCompletion($post_data);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理流式输出
|
|
|
+ foreach ($streamGenerator as $chunk) {
|
|
|
+ if (isset($chunk['type'])) {
|
|
|
+ if ($chunk['type'] === 'done') {
|
|
|
+ // 最终结果
|
|
|
+ $fullContent = $chunk['full_content'];
|
|
|
+ $fullReasoningContent = $chunk['full_reasoning'];
|
|
|
+ $usage = isset($chunk['usage']) ? $chunk['usage'] : [];
|
|
|
+ } else {
|
|
|
+ // 逐块yield数据
|
|
|
+ yield $chunk;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- dLog('deepseek')->info('流式读取完成', [
|
|
|
- 'content_length' => strlen($fullContent),
|
|
|
- 'reasoning_length' => strlen($fullReasoningContent)
|
|
|
- ]);
|
|
|
-
|
|
|
dLog('deepseek')->info('完整内容: '.$fullContent);
|
|
|
// 处理完整内容并返回最终结果
|
|
|
$episode_arr = handleEpisodeContent($fullContent);
|
|
|
@@ -3865,5 +3754,246 @@ class DeepSeekService
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用火山引擎对话(Chat) API
|
|
|
+ *
|
|
|
+ * @param array $params 包含以下参数:
|
|
|
+ * - model: 模型ID(必填)
|
|
|
+ * - messages: 消息列表(必填)
|
|
|
+ * - stream: 是否流式输出(可选,默认false)
|
|
|
+ * - max_tokens: 最大输出token数(可选)
|
|
|
+ * - temperature: 采样温度(可选,默认1)
|
|
|
+ * - top_p: 核采样概率(可选,默认0.7)
|
|
|
+ * - frequency_penalty: 频率惩罚系数(可选,默认0)
|
|
|
+ * - presence_penalty: 存在惩罚系数(可选,默认0)
|
|
|
+ * - stop: 停止词(可选)
|
|
|
+ * - tools: 工具列表(可选)
|
|
|
+ * @return array|Generator 非流式返回数组,流式返回生成器
|
|
|
+ */
|
|
|
+ public function volcEngineChatCompletion(array $params)
|
|
|
+ {
|
|
|
+ $apiKey = env('VOLC_AI_API_KEY');
|
|
|
+
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ Utils::throwError('20003:火山引擎API Key未配置,请检查环境变量VOLC_AI_API_KEY');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建请求参数
|
|
|
+ $requestData = [
|
|
|
+ 'model' => $params['model'] ?? 'deepseek-v3-2-251201',
|
|
|
+ 'messages' => $params['messages'] ?? [],
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (!in_array($requestData['model'], $this->valid_text_models)) {
|
|
|
+ Utils::throwError('20003:该模型不支持!');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加可选参数
|
|
|
+ if (isset($params['stream'])) {
|
|
|
+ $requestData['stream'] = $params['stream'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['stream_options'])) {
|
|
|
+ $requestData['stream_options'] = $params['stream_options'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['max_tokens'])) {
|
|
|
+ $requestData['max_tokens'] = $params['max_tokens'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['max_completion_tokens'])) {
|
|
|
+ $requestData['max_completion_tokens'] = $params['max_completion_tokens'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['temperature'])) {
|
|
|
+ $requestData['temperature'] = $params['temperature'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['top_p'])) {
|
|
|
+ $requestData['top_p'] = $params['top_p'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['frequency_penalty'])) {
|
|
|
+ $requestData['frequency_penalty'] = $params['frequency_penalty'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['presence_penalty'])) {
|
|
|
+ $requestData['presence_penalty'] = $params['presence_penalty'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['stop'])) {
|
|
|
+ $requestData['stop'] = $params['stop'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['tools'])) {
|
|
|
+ $requestData['tools'] = $params['tools'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['tool_choice'])) {
|
|
|
+ $requestData['tool_choice'] = $params['tool_choice'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['response_format'])) {
|
|
|
+ $requestData['response_format'] = $params['response_format'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['thinking'])) {
|
|
|
+ $requestData['thinking'] = $params['thinking'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($params['reasoning_effort'])) {
|
|
|
+ $requestData['reasoning_effort'] = $params['reasoning_effort'];
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->info('火山引擎对话API请求参数: ', $requestData);
|
|
|
+
|
|
|
+ $client = new Client(['verify' => false, 'timeout' => 1200]);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 判断是否为流式输出
|
|
|
+ $isStream = isset($params['stream']) && $params['stream'] === true;
|
|
|
+
|
|
|
+ if ($isStream) {
|
|
|
+ // 流式输出
|
|
|
+ return $this->volcEngineChatCompletionStream($client, $apiKey, $requestData);
|
|
|
+ } else {
|
|
|
+ // 非流式输出
|
|
|
+ $response = $client->post('https://ark.cn-beijing.volces.com/api/v3/chat/completions', [
|
|
|
+ 'headers' => [
|
|
|
+ 'Authorization' => 'Bearer ' . $apiKey,
|
|
|
+ 'Content-Type' => 'application/json',
|
|
|
+ ],
|
|
|
+ 'json' => $requestData
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody(), true);
|
|
|
+ dLog('deepseek')->info('火山引擎对话API响应: ', $responseData);
|
|
|
+
|
|
|
+ return $responseData;
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('火山引擎对话API请求异常: ' . $e->getMessage());
|
|
|
+ Utils::throwError('20003:火山引擎对话API请求失败: ' . $e->getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 火山引擎对话API流式输出
|
|
|
+ *
|
|
|
+ * @param Client $client HTTP客户端
|
|
|
+ * @param string $apiKey API密钥
|
|
|
+ * @param array $requestData 请求数据
|
|
|
+ * @return Generator
|
|
|
+ */
|
|
|
+ private function volcEngineChatCompletionStream($client, $apiKey, $requestData)
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $response = $client->post('https://ark.cn-beijing.volces.com/api/v3/chat/completions', [
|
|
|
+ 'headers' => [
|
|
|
+ 'Authorization' => 'Bearer ' . $apiKey,
|
|
|
+ 'Content-Type' => 'application/json',
|
|
|
+ ],
|
|
|
+ 'json' => $requestData,
|
|
|
+ 'stream' => true
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $body = $response->getBody();
|
|
|
+ $buffer = '';
|
|
|
+ $fullContent = '';
|
|
|
+ $fullReasoningContent = '';
|
|
|
+ $usage = [];
|
|
|
+
|
|
|
+ dLog('deepseek')->info('开始读取火山引擎对话API流式响应');
|
|
|
+
|
|
|
+ // 逐行读取流式响应
|
|
|
+ while (!$body->eof()) {
|
|
|
+ $chunk = $body->read(1024);
|
|
|
+ $buffer .= $chunk;
|
|
|
+
|
|
|
+ // 按行分割
|
|
|
+ $lines = explode("\n", $buffer);
|
|
|
+ $buffer = array_pop($lines);
|
|
|
+
|
|
|
+ foreach ($lines as $line) {
|
|
|
+ $line = trim($line);
|
|
|
+
|
|
|
+ if (empty($line)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是SSE数据行
|
|
|
+ if (strpos($line, 'data: ') !== 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $data = substr($line, 6);
|
|
|
+
|
|
|
+ if ($data === '[DONE]') {
|
|
|
+ dLog('deepseek')->info('收到结束标记');
|
|
|
+ break 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $json = json_decode($data, true);
|
|
|
+
|
|
|
+ if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
+ dLog('deepseek')->warning('JSON解析错误: ' . json_last_error_msg());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($json['choices'][0]['delta'])) {
|
|
|
+ $delta = $json['choices'][0]['delta'];
|
|
|
+
|
|
|
+ // 处理思维链内容
|
|
|
+ if (isset($delta['reasoning_content'])) {
|
|
|
+ $fullReasoningContent .= $delta['reasoning_content'];
|
|
|
+ yield [
|
|
|
+ 'type' => 'reasoning',
|
|
|
+ 'content' => $delta['reasoning_content'],
|
|
|
+ 'full_reasoning' => $fullReasoningContent
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理回答内容
|
|
|
+ if (isset($delta['content'])) {
|
|
|
+ $fullContent .= $delta['content'];
|
|
|
+ yield [
|
|
|
+ 'type' => 'content',
|
|
|
+ 'content' => $delta['content'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取使用统计信息
|
|
|
+ if (isset($json['usage'])) {
|
|
|
+ $usage = $json['usage'];
|
|
|
+ dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('deepseek')->info('流式读取完成', [
|
|
|
+ 'content_length' => strlen($fullContent),
|
|
|
+ 'reasoning_length' => strlen($fullReasoningContent)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ yield [
|
|
|
+ 'type' => 'done',
|
|
|
+ 'full_content' => $fullContent,
|
|
|
+ 'full_reasoning' => $fullReasoningContent,
|
|
|
+ 'usage' => $usage
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('deepseek')->error('火山引擎对话API流式请求异常: ' . $e->getMessage());
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|