[ 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ "journal_scope": { "assessment": "是/否", "explanation": "请详细解释说明" } }', "criteria" => "根据文章的标题:{title};摘要:{abstrart}以及期刊范围:{scope}来判断文章是否符合目标期刊{journal_name}" ], "attribute" => [ 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ "attribute": { "assessment": "是/否", "explanation": "请总结归纳分析" } }', "criteria" => "请结合以下几点【研究内容的原创性:论文中的研究内容是否与已有的研究重复?是否在同样的领域提出了类似的结论,但在方法或结果上有所创新?如果有,作者是否清楚地解释了如何与之前的研究不同,或者如何在原有基础上进行扩展或改进?如果是综述文章,汇总并综合最新的研究成果,尤其是近几年内的重要发现,展示领域内最新的进展成果。作者可以识别出未被充分讨论的问题或提出新的研究问题,而不是简单文献堆砌。文章中的图表创新能否将信息的清晰呈现,方便读者理解复杂研究问题。论文方法创新性评估要点:是否采用了新的实验模型或创新的实验设计,能有效解决当前研究中的难点或空白?是否有合理的对照组和多组实验设计,确保研究结果的可靠性?是否使用了当前前沿的技术(如高通量测序、CRISPR基因编辑等),提高了实验精度或数据分析能力?是否结合了跨学科的方法(如生物信息学、人工智能等)?是否应用了多种验证手段或统计方法,确保结果的可信度?是否通过细胞实验、动物模型等多重验证,确保实验结果的可靠性?结论与数据的创新性:研究结论是否提出了新观点或新见解?是否提供了新的实验数据或观察结果,能够突破当前的研究局限?例如,发现了新的生物标志物,或对已知生物通路的作用机制提供了全新的解释】评估文章内容{content}是否有科学前沿性和创新性?" ] //, // "contradiction" => [ // 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 // 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ // "contradiction": { // "assessment": "是/否", // "explanation": "请引用具体段落说明矛盾之处" // } // }', // "criteria" => "根据文章内容{content}分析是否前后矛盾或存在逻辑不一致的问题?" // ], // "unreasonable" => [ // 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 // 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ // "unreasonable": { // "assessment": "是/否", // "explanation": "包括实验设计、数据分析、结论推导等方面的问题" // } // }', // "criteria" => "根据文章内容{content}分析是否有明显的不合理之处?" // ] ]; //定义redis连接 private $redis; private $oQueueRedis; public function __construct() { // 初始化 Redis 连接 $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']); $this->oQueueRedis = QueueRedis::getInstance(); } /** * 构建公微模版-处理提示词【默认】 */ public function buildDefaultPrompt($aSearch = []){ if(empty($aSearch)){ return []; } $sSysMessagePrompt = ' 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,请返回中文解释!返回格式必须严格遵循以下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 = []){ if(empty($aSearch)){ return []; } $sSysMessagePrompt = ' 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,按照以下严格格式生成结构化输出[中文]:'; $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 = []){ if(empty($aSearch)){ return []; } $sSysMessagePrompt = '你是一位专业的医学翻译专家,请将用户提供的内容准确、流畅地翻译成中文。翻译需自然流畅、口语化、连贯性、学术性,保留原文的专业术语和逻辑结构'; $sUserPrompt = '"将以下内容翻译为中文,仅返回翻译结果,不要解释:\n {#content#}"'; $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); return [ ['role' => 'user', 'content' => $sUserPrompt] ]; } /** * 构建AI审稿-处理提示词【重要的多次请求问题初始化】 */ public function buildReviewPromptImportant($aSearch = [],$aValue = []) { //必填验证 if(empty($aSearch) || empty($aValue)){ return []; } //组装问题 $sUserPrompt = empty($aValue['criteria']) ? '' : $aValue['criteria']; $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); $aMessage = [ ['role' => 'system', 'content' => $aValue['system']], ['role' => 'user', 'content' => $sUserPrompt] ]; //模型版本 $model = empty($aSearch['model']) ? 'gpt-4.1' : $aSearch['model']; $aMessage = [ 'model' => $model, 'messages' => $aMessage, 'temperature' => 0.2,// 降低随机性(0-1,0为最确定) ]; return $aMessage; } /** * 构建AI审稿-处理提示词【非重要的一次请求回答】 */ public function buildReviewPromptUnimportant($aSearch = []) { if(empty($aSearch)){ return []; } $sSysMessagePrompt = ' 你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。请根据提供的信息,对以下问题进行专业评审 ```json { "ethics": "根据文章内容分析是否存在伦理号缺失或明显伦理问题?", "academic": "根据文章内容分析是否存在学术不端问题【抄袭、数据作假、图片伪造等】?", "conclusion": "根据文章内容判断文章结论科学性和可靠性?", "fund_number":"根据文章内容分析有无基金号?请详细说明", "hotspot":"根据文章内容分析有哪些符合目标期刊当下的热点话题", "submit_direction":"根据文章内容总结文章送审方向", "references_num":"根据文章内容统计文章参考文献的数量", "references_past_three":"统计文章内容里近3年的参考文献的数量及所占比例", "references_past_five":"统计文章内容里近5年的参考文献的数量及所占比例", "references_ratio_JCR1":"根据2024JCR最新分区分析文章内容里的文献判断属于JCR1还是JCR2,统计属于JCR 1区的数量及比例", "references_ratio_JCR2":"根据2024JCR最新分区评估文章内容里的文献判断属于JCR1还是JCR2,统计属于JCR 2区的数量及比例", "registration_assessment":"根据文章内容分析是否存在临床注册号和知情同意书?解释说明", "cite_rate" => "根据文章内容分析文章发表后被引用的概率" }请针对每个问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下 JSON 结构:{ "ethics": { "assessment": "是/否", "explanation": "请针对文章伦理号及伦理进行分析及说明" }, "academic": { "assessment": "是/否", "explanation": "请进行相似度分析并指出具体可疑部分" }, "conclusion": { "assessment": "是/否", "explanation": "是否基于充分证据得出" }, "fund_number": "", "hotspot": "", "submit_direction": "", "references_num":"", "references_past_three": "", "references_past_five": "", "references_ratio_JCR1": "", "references_ratio_JCR2": "", "registration_assessment":"", "cite_rate" => "" }'; $sUserPrompt = '文章内容{content};目标期刊{journal_name}'; $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); return [ ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] ]; } /** * CURL 发送请求到 OpenAI【单独】 * @param $messages 内容 * @param $model 模型类型 */ public function curlOpenAI($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.1' : $aParam['model']; //接口地址 $sUrl = $this->sUrl; //组装数据 $data = [ 'model' => $model, 'messages' => $aMessage, 'temperature' => 0.2,// 降低随机性(0-1,0为最确定) ]; $this->curl = curl_init(); // 通用配置 curl_setopt($this->curl, CURLOPT_URL, $sUrl); // 设置头信息 curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $this->sApiKey ]); 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); //设置为POST方式 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); curl_close($this->curl); return json_encode(['status' => 3,'OPENAI Error:'.$this->sError]); } $aResult = json_decode($result,true); //处理返回信息 $aData = empty($aResult['choices']) ? [] : $aResult['choices']; if(empty($aData)){ return json_encode(['status' => 5,'msg' => 'OPENAI returns empty content']); } $aData = empty($aData[0]) ? [] : $aData[0]; if(empty($aData)){ return json_encode(array('status' => 6,'msg' => 'OPENAI did not return data')); } $aData = empty($aData['message']) ? [] : $aData['message']; $aData = empty($aData['content']) ? [] : $aData['content']; if(empty($aData)){ return json_encode(array('status' => 7,'msg' => 'OPENAI did not return data')); } //数据转换 $aData = $this->extractAndParse($aData); $aContent = empty($aData['data']) ? [] : $aData['data']; $sMsg = empty($aData['msg']) ? 'OPENAI did not return data' : $aData['msg']; if(empty($aContent)){ return json_encode(array('status' => 8,'msg' => $sMsg)); } curl_close($this->curl); return json_encode(['status' => 1,'msg' => 'success','data' => $aContent]); } /** * 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 = empty($aParam['url']) ? $this->sUrl : $aParam['url']; //组装数据 $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; } /** * 微信公众号-生成公微内容(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->oQueueRedis->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; $iProgress = $this->oQueueRedis->updateProcessingProgress($sRedisKey,$iIndex + 1); //保存内容 $sRedisKey = 'ai_create_article_progress_'.$iId; $this->oQueueRedis->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)){//更新AI审稿记录表 if($iProgress >= 100){ $aData['is_generate'] = 1; } $aData['article_id'] = $iId; $this->updateAiContent($aData); } return $aResult; } /** * 微信公众号-更新AI生成内容 */ private function updateAiContent($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']); } //更新生成状态 $oArticle = new Article; $aResult = json_decode($oArticle->updateAiArticle($aParam),true); $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg']; //是否生成 $is_generate = empty($aParam['is_generate']) ? 2 : $aParam['is_generate']; //内容生成完成推送上传素材队列 if($is_generate == 1){ 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 = $oMaterial->addWechatLog($aLogInfo); } return json_encode($aResult); } /** * 添加接口访问日志 */ private function addLog($aParam = []){ $aField = ['open_ai_id','log_data','empty_data']; $aInsert = []; foreach ($aField as $key => $value) { if(isset($aParam[$value])){ $aInsert[$value] = is_array($aParam[$value]) ? json_encode($aParam[$value]) : $aParam[$value]; } } $result = 0; if(empty($aInsert)){ return true; } $aInsert['create_time'] = time(); return DB::name('openapi_log')->insertGetId($aInsert); } /** * 从文本中提取被```json```和```包裹的JSON内容并解析 * @param string $text 包含JSON代码块的文本 * @param bool $assoc 是否返回关联数组(默认true) * @return array|object 解析后的JSON数据,失败时返回null */ public 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]; } }