diff --git a/application/common/OpenAi.php b/application/common/OpenAi.php index 6870dc9..85c27a4 100644 --- a/application/common/OpenAi.php +++ b/application/common/OpenAi.php @@ -2,17 +2,16 @@ namespace app\common; use think\Cache; use think\Db; - +use think\Queue; class OpenAi { protected $sApiKey = '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'; protected $proxy = ''; - protected $sUrl = 'https://api.openai.com/v1/chat/completions'; + protected $sUrl = 'http://chat.taimed.cn/v1/chat/completions';//'https://api.openai.com/v1/chat/completions'; protected $curl; protected $sResponesData; protected $sError; - protected $timeout = 60; - + protected $timeout = 300; //JAVA接口 protected $sJavaUrl = "http://ts.tmrjournals.com/"; //官网文件地址 @@ -21,7 +20,6 @@ class OpenAi protected $sTmrUrl = "http://journalapi.tmrjournals.com/public/index.php";//"http://zmzm.journal.dev.com/";//; protected $aArticleImportantPrompt = [ "journal_scope" => [ - 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ "journal_scope": { @@ -32,7 +30,6 @@ class OpenAi "criteria" => "根据文章的标题:{title};摘要:{abstrart}以及期刊范围:{scope}来判断文章是否符合目标期刊{journal_name}" ], "attribute" => [ - 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ "attribute": { @@ -41,90 +38,144 @@ class OpenAi } }', "criteria" => "请结合以下几点【研究内容的原创性:论文中的研究内容是否与已有的研究重复?是否在同样的领域提出了类似的结论,但在方法或结果上有所创新?如果有,作者是否清楚地解释了如何与之前的研究不同,或者如何在原有基础上进行扩展或改进?如果是综述文章,汇总并综合最新的研究成果,尤其是近几年内的重要发现,展示领域内最新的进展成果。作者可以识别出未被充分讨论的问题或提出新的研究问题,而不是简单文献堆砌。文章中的图表创新能否将信息的清晰呈现,方便读者理解复杂研究问题。论文方法创新性评估要点:是否采用了新的实验模型或创新的实验设计,能有效解决当前研究中的难点或空白?是否有合理的对照组和多组实验设计,确保研究结果的可靠性?是否使用了当前前沿的技术(如高通量测序、CRISPR基因编辑等),提高了实验精度或数据分析能力?是否结合了跨学科的方法(如生物信息学、人工智能等)?是否应用了多种验证手段或统计方法,确保结果的可信度?是否通过细胞实验、动物模型等多重验证,确保实验结果的可靠性?结论与数据的创新性:研究结论是否提出了新观点或新见解?是否提供了新的实验数据或观察结果,能够突破当前的研究局限?例如,发现了新的生物标志物,或对已知生物通路的作用机制提供了全新的解释】评估文章内容{content}是否有科学前沿性和创新性?" - ], - "contradiction" => [ + ] + //, + // "contradiction" => [ - 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 - 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ - "contradiction": { - "assessment": "是/否", - "explanation": "请引用具体段落说明矛盾之处" - } - }', - "criteria" => "根据文章内容{content}分析是否前后矛盾或存在逻辑不一致的问题?" - ], - "unreasonable" => [ + // 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 + // 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ + // "contradiction": { + // "assessment": "是/否", + // "explanation": "请引用具体段落说明矛盾之处" + // } + // }', + // "criteria" => "根据文章内容{content}分析是否前后矛盾或存在逻辑不一致的问题?" + // ], + // "unreasonable" => [ - 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 - 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ - "unreasonable": { - "assessment": "是/否", - "explanation": "包括实验设计、数据分析、结论推导等方面的问题" - } - }', - "criteria" => "根据文章内容{content}分析是否有明显的不合理之处?" - ] + // 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 + // 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ + // "unreasonable": { + // "assessment": "是/否", + // "explanation": "包括实验设计、数据分析、结论推导等方面的问题" + // } + // }', + // "criteria" => "根据文章内容{content}分析是否有明显的不合理之处?" + // ] ]; + //定义redis连接 + private $redis; + public function __construct() + { + + $config = \think\Config::get('queue'); + $this->redis = new \Redis(); + $this->redis->connect($config['host'], $config['port']); + + if (!empty($config['password'])) { + $this->redis->auth($config['password']); + } + + $this->redis->select($config['select']); + // 初始化 Redis 连接 + // $this->redis = Cache::store('redis')->handler(); + } /** * 构建公微模版-处理提示词【默认】 */ - public function buildDefaultPrompt($aSearch = []) - { + public function buildDefaultPrompt($aSearch = []){ + if(empty($aSearch)){ return []; } - $sSysMessagePrompt = ' - 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,按照以下严格格式生成结构化输出[中文]【必须严格遵循以下 JSON 结构】: - { - "covered": "【列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析】", - "digest": "【学术规范翻译并提炼摘要,强调逻辑性、科学术语准确性和表达严谨性,采用段落形式,总字数不超过500字】", - "research_background": "【提炼研究背景,采用连贯的段落形式,总字数超过200字】", - "discussion_results": "【针对文章简单总结讨论和结果,采用连贯的段落形式,总字数超过450字】", - "research_method": "【总结文章的研究方法,采用连贯的段落形式,总字数超过300字】", - "prospect": "【针对稿件内容进行展望撰写,采用连贯的段落形式】", - "highlights": "【总结归纳亮点,至少3点,每点用分号分隔】", - "title_chinese": "【将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性】", - "content": "【将内容翻译成中文,需自然流畅、口语化、连贯性、学术性,保留原文的章节结构和图表编号】" - }'; - $sUserPrompt = '标题:{#title_chinese#} 摘要: {#abstract#} 内容: {#content#}'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - return [ - ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] + 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,请返回中文解释!返回格式必须严格遵循以下JSON结构:'; + $aQuestion = [ + "covered" => "[列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析]", + "digest" => "[学术规范翻译并提炼摘要,强调逻辑性、科学术语准确性和表达严谨性,采用段落形式,总字数不超过500字]", + "research_background" => "[提炼研究背景,采用连贯的段落形式,总字数超过200字]", + "discussion_results" => "[针对文章简单总结讨论和结果,采用连贯的段落形式,总字数超过450字]", + "research_method" => "[总结文章的研究方法,采用连贯的段落形式,总字数超过300字]", + "prospect" => "[针对稿件内容进行展望撰写,采用连贯的段落形式]", + "highlights" => "[总结归纳亮点,至少3点,每点用分号分隔]", + "title_chinese" => "[将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性]" + // , + // "content" => "将内容翻译成中文,需自然流畅、口语化、连贯性、学术性,保留原文的章节结构和图表编号" ]; + //问题处理 + $aMessage = []; + foreach($aQuestion as $key => $value){ + //修改当前内容 + $sInfo = json_encode([$key => $value],JSON_UNESCAPED_UNICODE); + $sSysMessagePromptInfo = $sSysMessagePrompt.$sInfo; + if($key == "title_chinese"){ + $sUserPrompt = '{#title_chinese#}'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + } + if($key == "content"){ + $sUserPrompt = '{#content#}'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + } + if(!in_array($key,["title_chinese","content"])){ + $sUserPrompt = '标题:{#title_chinese#} 摘要: {#abstract#} 内容: {#content#}'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + } + + $aMessage[] = [ + ['role' => 'system', 'content' => $sSysMessagePromptInfo], + ['role' => 'user', 'content' => $sUserPrompt] + ]; + } + return $aMessage; } /** * 构建公微模版-处理提示词【Review】 */ - public function buildReviewPrompt($aSearch = []) - { + public function buildReviewPrompt($aSearch = []){ if(empty($aSearch)){ return []; } - $sSysMessagePrompt = ' - 你是一位专业的医学学术翻译与传播专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,按照以下严格格式生成结构化[中文]输出【必须严格遵循以下 JSON 结构】: - { - "covered": "【列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析】", - "overview": "【按照学术规范翻译并提炼文章概述,整体内容应大于1200字,其中应包含文章背景(不少于400字),其他内容提炼更强调逻辑性、科学术语准确性和表达的严谨性,注意内容不要有严重重复,采用连贯的段落形式】", - "summary": "【针对文章结论生成一个简单总结,内容不要和文章概述重复,字数150以内】", - "title_chinese": "【将标题翻译成中文,需自然流畅、口语化、连贯性、学术性】", - "content": "【将文章内容翻译成中文,需自然流畅、口语化、连贯性、学术性】" - }'; - $sUserPrompt = '标题:{#title_chinese#} 摘要: {#abstract#} 内容: {#content#}'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - return [ - ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] + 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,按照以下严格格式生成结构化输出[中文]:'; + $aQuestion = [ + "covered" => "列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析", + "overview" => "按照学术规范翻译并提炼文章概述,整体内容应大于1200字,其中应包含文章背景(不少于400字),其他内容提炼更强调逻辑性、科学术语准确性和表达的严谨性,注意内容不要有严重重复,采用连贯的段落形式", + "summary" => "针对文章结论生成一个简单总结,内容不要和文章概述重复,字数150以内", + "title_chinese" => "将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性" + // , + // "content" => "将内容翻译成中文,需自然流畅、口语化、连贯性、学术性,保留原文的章节结构和图表编号" ]; + //问题处理 + $aMessage = []; + foreach($aQuestion as $key => $value){ + $sInfo = json_encode([$key => $value],JSON_UNESCAPED_UNICODE); + $sSysMessagePromptInfo = $sSysMessagePrompt.$sInfo; + if($key == "title_chinese"){ + $sUserPrompt = '{#title_chinese#}'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + } + if($key == "content"){ + $sUserPrompt = '{#content#}'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + } + if(!in_array($key,["title_chinese","content"])){ + $sUserPrompt = '标题:{#title_chinese#} 摘要: {#abstract#} 内容: {#content#}'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + } + + $aMessage[] = [ + ['role' => 'system', 'content' => $sSysMessagePromptInfo], + ['role' => 'user', 'content' => $sUserPrompt] + ]; + } + return $aMessage; } - /** * 构建AI翻译-处理提示词 */ - public function buildTranslatePrompt($aSearch = []) - { + public function buildTranslatePrompt($aSearch = []){ if(empty($aSearch)){ return []; } @@ -136,7 +187,6 @@ class OpenAi ]; } - /** * 构建AI审稿-处理提示词【重要的多次请求问题初始化】 */ @@ -217,34 +267,6 @@ class OpenAi ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] ]; } - /** - * 从文本中提取被```json```和```包裹的JSON内容并解析 - * @param string $text 包含JSON代码块的文本 - * @param bool $assoc 是否返回关联数组(默认true) - * @return array|object 解析后的JSON数据,失败时返回null - */ - private function extractAndParse($text, $assoc = true) - { - - // 使用正则表达式提取JSON代码块 - preg_match('/```json\s*(\{.*?\})\s*```/s', $text, $matches); - $jsonContent = empty($matches[1]) ? '' : $matches[1]; - if (empty($jsonContent)) { - // 尝试宽松匹配(允许没有json标记) - preg_match('/```\s*(\{.*?\})\s*```/s', $text, $matches); - $jsonContent = empty($matches[1]) ? $text : $matches[1]; - } - - // 解析JSON - $aData = json_decode($jsonContent, $assoc); - - // 检查解析是否成功 - if (json_last_error() !== JSON_ERROR_NONE) { - return ['status' => 2,'msg' => "API返回无效JSON: " . json_last_error_msg()]; - } - - return ['status' => 1,'msg' => 'success','data' => $aData]; - } /** * CURL 发送请求到 OpenAI【单独】 @@ -253,7 +275,6 @@ class OpenAi */ public function curlOpenAI($aParam = []){ - //询问AI信息 $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; if(empty($aMessage)){ @@ -288,9 +309,7 @@ class OpenAi curl_setopt($this->curl, CURLOPT_POSTFIELDS,json_encode($data)); curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, TRUE) ; // 获取数据返回 // curl_setopt($this->curl, CURLOPT_TIMEOUT, $this->timeout); - $result = curl_exec($this->curl); - //请求失败 if (curl_errno($this->curl)){ $this->sError = curl_errno($this->curl); @@ -325,7 +344,7 @@ class OpenAi /** * 对接OPENAI接口-并行CURL请求【重要维度单独询问】 */ - public function curlMultiOpenAIImportant($aSearch = [],$timeout = 60, $iChunkSize = 5) { + public function curlMultiOpenAIImportant($aSearch = [],$timeout = 120, $iChunkSize = 2) { // 入参校验 if (empty($aSearch)) { return json_encode(['status' => 2, 'msg' => 'Parameter is empty']); @@ -373,18 +392,19 @@ class OpenAi CURLOPT_URL => $this->sUrl, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', - 'Authorization: Bearer ' . $this->sApiKey + 'Authorization: Bearer ' . $this->sApiKey, + 'Expect:', ], CURLOPT_PROXY => $this->proxy, // SSL验证优化:若代理证书不可信,临时关闭(生产环境需配置信任证书) - CURLOPT_SSL_VERIFYPEER => false, // 调试时设为false,生产环境设为true - CURLOPT_SSL_VERIFYHOST => 0, // 调试时设为0,生产环境设为2 + CURLOPT_SSL_VERIFYPEER => true, // 调试时设为false,生产环境设为true + CURLOPT_SSL_VERIFYHOST => 2, // 调试时设为0,生产环境设为2 CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($aQuestionInfo), CURLOPT_RETURNTRANSFER => true, // 超时优化:延长响应超时,新增连接超时 CURLOPT_TIMEOUT => $timeout, // 总超时(秒),建议60-120 - CURLOPT_CONNECTTIMEOUT => 10, // 连接超时(秒),避免无限等待 + CURLOPT_CONNECTTIMEOUT => 20, // 连接超时(秒),避免无限等待 CURLOPT_LOW_SPEED_LIMIT => 1024, // 最低速度(字节/秒),低于此值触发超时 CURLOPT_LOW_SPEED_TIME => 30, // 持续低速时间(秒),超过则终止 ]); @@ -516,14 +536,228 @@ class OpenAi //日志记录 $this->addLog($aParam); + return json_encode($aParam); } + /** + * CURL 发送请求到 OpenAI【流式】 + * @param $messages 内容 + * @param $model 模型类型 + */ + public function curlOpenAIStream($aParam = []){ + //询问AI信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if(empty($aMessage)){ + return json_encode(['status' => 2,'msg' => 'AI Q&A content not obtained']); + } + + //模型 + $model = empty($aParam['model']) ? 'gpt-4' : $aParam['model']; + //超时设置 + $timeout = empty($aParam['timeout']) ? 300 : $aParam['timeout']; + //接口地址 + $sUrl = $this->sUrl; + + //组装数据 + $data = [ + 'model' => $model, + 'messages' => $aMessage, + 'temperature' => 0.2,// 降低随机性(0-1,0为最确定) + 'stream' => true // 关键:启用流式传输,避免超时 + ]; + + // Curl通用配置 + $this->curl = curl_init(); + curl_setopt($this->curl, CURLOPT_URL, $this->sUrl); + curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->sApiKey + ]); + // 代理与SSL配置(根据你的服务器环境调整) + curl_setopt($this->curl, CURLOPT_PROXY, $this->proxy); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, 2); + // 超时与传输配置 + curl_setopt($this->curl, CURLOPT_POST, true); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($this->curl, CURLOPT_TIMEOUT, $timeout); // CURL超时 + curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, 30); // 连接超时 + + // === 5. 流式响应处理(核心避免超时) === + $streamContent = ''; // 累积流式返回的内容 + // 回调函数:每收到一块数据就处理并保存,避免整段等待 + curl_setopt($this->curl, CURLOPT_WRITEFUNCTION, function ($curl, $data) use (&$streamContent) { + $streamContent .= $data; + return strlen($data); // 必须返回数据长度,否则CURL会中断 + }); + + //执行请求 + $result = curl_exec($this->curl); + //获取错误信息 + $curlErrno = curl_errno($this->curl); + //获取Code码 + $httpCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + //关闭连接 + curl_close($this->curl); + + //错误处理 + if (!empty($curlErrno)) { + // 超时但已有部分数据:保存进度,下次从该块重试 + if ($curlErrno == CURLE_OPERATION_TIMEDOUT && !empty($streamContent)) { + return json_encode([ + 'status' => 3, + 'msg' => "处理超时,已保存进度", + ]); + } + // 其他错误(如网络问题) + return json_encode([ + 'status' => 4, + 'msg' => "OPENAI Error:".curl_error($this->curl) + ]); + } + + //处理流式结果 + $sStreamResponse = $this->parseMedicalStreamResponse($streamContent); + return json_encode(['status' => 1,'msg' => 'success','data' => $sStreamResponse]); + } + /** + * 解析流式响应 + */ + private function parseMedicalStreamResponse($streamContent) + { + $fullContent = ''; + $lines = explode("\n", $streamContent); + foreach ($lines as $line) { + $line = trim($line); + if (strpos($line, 'data: ') === 0 && $line !== 'data: [DONE]') { + $jsonStr = substr($line, 6); // 去掉"data: "前缀 + $jsonData = json_decode($jsonStr, true); + $fullContent .= $jsonData['choices'][0]['delta']['content'] ?? ''; + } + } + return $fullContent; + } + + /** + * 记录处理进度【Redis】 + */ + private function recordProcessingStart($key,$totalQuestions) + { + $this->redis->hMSet($key, [ + 'status' => 'processing', + 'total' => $totalQuestions, + 'completed' => 0, + 'start_time' => time() + ]); + $this->redis->expire($key, 86400); // 24小时过期 + } + /** + * 更新处理进度【Redis】 + */ + private function updateProcessingProgress($key,$iId,$completed) + { + + $this->redis->hSet($key, 'completed', $completed); + //完成进度 + $iProgress = round(($completed / $this->redis->hGet($key, 'total')) * 100, 2); + if($iProgress == 100){ + $this->recordProcessingComplete($key,$iId); + } + $this->redis->hSet($key, 'progress', $iProgress); + } + /** + * 记录处理完成【Redis】 + */ + private function recordProcessingComplete($key,$iId) + { + $this->redis->hSet($key, 'status', 'completed'); + $this->redis->hSet($key, 'end_time', time()); + $this->wechatGegnerate(['article_id' => $iId]); + } + + /** + * 保存分块进度【Redis】 + */ + private function saveChunkProgress($key, $chunkIndex, $content) + { + $this->redis->hset($key, "chunk_{$chunkIndex}", $content); + $this->redis->expire($key, 86400); // 进度保存24小时 + } + + /** + * 微信公众号-生成公微内容(CURL) + */ + public function createWechatContent($aParam = []) + { + //主键ID + $iId = empty($aParam['redis_id']) ? 0 : $aParam['redis_id']; + if(empty($iId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //提问信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if (empty($aMessage)) { + return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']); + } + //记录处理开始 + $iNum = count($aMessage); + $sRedisKey = 'ai_create_article_'.$iId; + $this->recordProcessingStart($sRedisKey,$iNum); + //定义空数组 + $aChunkResult = $aFail = []; + foreach ($aMessage as $key => $value) { + $aParam['messages'] = $value; + $aParam['chunkIndex'] = $key; + $aParam['count_num'] = $iNum; + Queue::push('app\api\job\createFieldForQueue@fire', $aParam, 'createFieldForQueue'); + } + return json_encode(['status' => 1, 'msg' => 'createFieldForQueue success']); + } + /** + * 微信公众号-生成内容队列形式 + */ + public function createFieldForQueue($aParam = []){ + + //主键ID + $iId = empty($aParam['redis_id']) ? 0 : $aParam['redis_id']; + if(empty($iId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //提问信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if (empty($aMessage)) { + return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']); + } + //最大执行数 + $iMaxNum = empty($aParam['count_num']) ? 0 : $aParam['count_num']; + //请求OPENAI + $aResult = $this->curlOpenAIStream($aParam); + + //更新处理进度 + $iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex']; + $sRedisKey = 'ai_create_article_'.$iId; + $this->updateProcessingProgress($sRedisKey,$iId,$iIndex + 1); + + //保存内容 + $sRedisKey = 'ai_create_article_progress_'.$iId; + $this->saveChunkProgress($sRedisKey, $iIndex,$aResult); + + //更新入库 + $aReturnData = json_decode($aResult,true); + $aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data']; + $aData = empty($aDataInfo) ? [] : $this->extractAndParse($aDataInfo); + $aData = empty($aData['data']) ? [] : $aData['data']; + if(!empty($aData)){ + $aData['article_id'] = $iId; + $this->updateAiArticle($aData); + } + return $aResult; + } /** * 获取期刊内容 */ - public function getJournalPaperArt($aParam = []){ //判断文章ID @@ -585,4 +819,106 @@ class OpenAi return DB::name('openapi_log')->insertGetId($aInsert); } + /** + * 更新AI生成内容入库 + * @param $messages 内容 + * @param $model 模型类型 + */ + private function updateAiArticle($aParam = []){ + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + + //查询内容是否存在 + $aWhere = ['is_delete' => 2]; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); + } + $aWhere['article_id'] = $iArticleId; + $aAiArticle = Db::name('ai_article')->field('ai_article_id')->where($aWhere)->find(); + if(empty($aAiArticle)){ + return json_encode(['status' => 3,'msg' => 'he article content of WeChat official account has not been generated']); + } + $iAiArticleId = $aAiArticle['ai_article_id']; + + //必填参数验证 + $aFields = ['article_id','title_english','title_chinese','journal_issn','covered','digest','research_result','content','highlights','discussion','prospect','research_background','discussion_results','research_method','overview','summary','is_generate']; + $sFiled = ''; + $aUpdateParam = []; + foreach($aFields as $val){ + if(!isset($aParam[$val])){ + continue; + } + if(is_array($aParam[$val])){ + $aParam[$val] = implode(";",$aParam[$val]); + } + $aUpdateParam[$val] = empty($aParam[$val]) ? '' : addslashes($aParam[$val]); + } + if(empty($aUpdateParam)){ + return json_encode(['status' => 1,'msg' => 'No data currently being processed']); + } + //执行入库 + $aUpdateParam['update_time'] = time(); + $result = Db::name('ai_article')->where('ai_article_id',$iAiArticleId)->limit(1)->update($aUpdateParam); + if($result === false){ + return json_encode(['status' => 4,'msg' => 'UPDATEING AI article failed']); + } + return json_encode(['status' => 1,'msg' => 'No data currently being processed']); + } + + private function wechatGegnerate($aParam = []){ + + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); + } + //更新生成状态 + $aParam['is_generate'] = 1; + $aResult = json_decode($this->updateAiArticle($aParam),true); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg']; + if($iStatus == 1){ + //四小时后推送上传素材并推送草稿箱 + $iDelaySeconds = 4 * 3600; // 4小时的秒数 + Queue::later($iDelaySeconds,'app\api\job\WechatMaterial@fire', ['article_id' => $iArticleId], 'WechatMaterial'); + $sMsg = '文章AI内容生成成功'; + }else{ + $iStatus = 2; + } + + //插入日志记录 + $oMaterial = new Material; + $aLogInfo = ['article_id' => $iArticleId,'type' => 5,'msg' =>$sMsg,'status' => $iStatus,'create_time' => time()]; + $result = json_decode($oMaterial->addWechatLog($aLogInfo),true); + return json_encode($aResult); + } + + /** + * 从文本中提取被```json```和```包裹的JSON内容并解析 + * @param string $text 包含JSON代码块的文本 + * @param bool $assoc 是否返回关联数组(默认true) + * @return array|object 解析后的JSON数据,失败时返回null + */ + private function extractAndParse($text, $assoc = true){ + + // 使用正则表达式提取JSON代码块 + preg_match('/```json\s*(\{.*?\})\s*```/s', $text, $matches); + $jsonContent = empty($matches[1]) ? '' : $matches[1]; + if (empty($jsonContent)) { + // 尝试宽松匹配(允许没有json标记) + preg_match('/```\s*(\{.*?\})\s*```/s', $text, $matches); + $jsonContent = empty($matches[1]) ? $text : $matches[1]; + } + + // 解析JSON + $aData = json_decode($jsonContent, $assoc); + + // 检查解析是否成功 + if (json_last_error() !== JSON_ERROR_NONE) { + return ['status' => 2,'msg' => "API返回无效JSON: " . json_last_error_msg()]; + } + + return ['status' => 1,'msg' => 'success','data' => $aData]; + } + }