1370 lines
62 KiB
PHP
1370 lines
62 KiB
PHP
<?php
|
||
namespace app\common;
|
||
use think\Cache;
|
||
use think\Db;
|
||
use think\Queue;
|
||
use app\common\Article;
|
||
use app\common\QueueRedis;
|
||
use app\common\HelperFunction;
|
||
class OpenAi
|
||
{
|
||
protected $sApiKey = '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';
|
||
protected $proxy = '';
|
||
protected $sUrl = 'http://chat.taimed.cn/v1/chat/completions';//'https://api.openai.com/v1/chat/completions';
|
||
//Ai地址
|
||
protected $sAiUrl = "http://125.39.141.154:10002";
|
||
protected $curl;
|
||
protected $sResponesData;
|
||
protected $sError;
|
||
protected $timeout = 300;
|
||
//JAVA接口
|
||
protected $sJavaUrl = "http://ts.tmrjournals.com/";
|
||
//官网文件地址
|
||
protected $sFileUrl = "https://submission.tmrjournals.com/public/";
|
||
//官网接口地址
|
||
protected $sTmrUrl = "http://journalapi.tmrjournals.com/public/index.php";//"http://zmzm.journal.dev.com/";//;
|
||
//公微问题模版
|
||
protected $aWechatQuestion = [
|
||
|
||
'system_message' => '您是一位医学期刊的医学科普转化专家,严格遵循用户要求的结构、语言和专业约束,不编造数据,不夸大结论,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,严格按要求生成内容[中文、可解析的JSON结构]:',
|
||
'public_message' => [
|
||
"covered" => "[列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析]",
|
||
"title_chinese" => "[将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性]"
|
||
],
|
||
'default' => [
|
||
"digest" => "[学术规范翻译并提炼摘要,强调逻辑性、科学术语准确性和表达严谨性,采用段落形式,注意内容不要和文章内容有严重重复,总字数不超过500字]",
|
||
"research_background" => "[提炼研究背景,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过200字]",
|
||
"results" => "[针对结果进行简单总结,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过450字]",
|
||
"discussion" => "[针对讨论进行简单总结,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过450字]",
|
||
"research_method" => "[针对研究方法进行简单总结,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过300字]",
|
||
"discussion" => "[针对讨论进行简单总结,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过450字]",
|
||
"conclusion" => "[针对结论进行简单总结,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过450字]",
|
||
],
|
||
'review' => [
|
||
"overview" => "按照学术规范翻译并提炼文章概述,整体内容应大于1200字,其中应包含文章背景(不少于400字),其他内容提炼更强调逻辑性、科学术语准确性和表达的严谨性,注意内容不要和文章内容有严重重复,采用连贯的段落形式",
|
||
"summary" => "针对文章结论生成一个简单总结,内容不要和文章概述重复,字数150以内",
|
||
]
|
||
];
|
||
//AI审稿提示词
|
||
private $aReviewQuestion = [
|
||
'system_message' => '您是一位资深的医学期刊学术评审专家,请负责严谨、客观地评估学术文章。请根据提供的医学论文信息,严格按要求生成内容[中文、可解析的JSON结构]:',
|
||
'A' => [
|
||
'journal_scope' => '结合标题:title摘要:abstrart以及期刊范围journal_scope来判断文章是否符合目标期刊targeted_journals',
|
||
'other_journal' =>'结合标题:title摘要:abstrart从期刊列表other_journal中根据期刊范围筛选出最符合的期刊【小于等于1个】'
|
||
],
|
||
'B' => [
|
||
'ai_ethics' => '结合标题:title摘要:abstrart分析',
|
||
'ethics' => '结合标题:title摘要:abstrart内容:content分析',
|
||
'ai_registration' => '结合标题:title摘要:abstrart分析',
|
||
'registration' => '标题:title摘要:abstrart内容content分析',
|
||
'academic' => '分析content是否存在学术不端问题【包括但不限于:1. 抄袭:是否存在未经引用的重复内容2. 数据作假:数据是否矛盾、不合理或无来源3. 图片伪造:是否提及可疑的图片/图表(如无原始数据支持)】',
|
||
'contradiction' => '分析content是否存在前后矛盾或逻辑不一致的问题【包括但不限于1. 性别与疾病不匹配(如女性疾病出现男性患者、男性疾病出现女性患者)2. 数量不一致(如病人数量、动物数量在不同位置描述冲突)3. 种类不一致(如实验动物种类前后描述不同)4. 其他明显的逻辑矛盾(如时间线冲突、因果关系矛盾)】',
|
||
'fund_number' => '分析content',
|
||
],
|
||
'C' => [
|
||
'attribute' => '分析content',
|
||
'hotspot' => '结合标题:title摘要:abstrart从领域列表hotspot分析是否包含热点领域',
|
||
'reference' => '分析reference参考文献数量',
|
||
]
|
||
];
|
||
private $aScopeReturn = [
|
||
'journal_scope' => [
|
||
'journal_scope_assessment' => '是/否',
|
||
'journal_scope_explanation' => '请详细解释说明'
|
||
],
|
||
'other_journal' => [
|
||
"other_journal_assessment" => "是/否",
|
||
"other_journal_issn" => "期刊issn",
|
||
'other_journal_explanation' => '请详细解释说明'
|
||
],
|
||
'ai_ethics' => [
|
||
'ethics_assessment' => "结合标题和摘要判断是否需要伦理号?[返回是/否]",
|
||
'ethics_explanation' => "[结合标题和摘要解释说明是否需要伦理号][返回格式字符串]",
|
||
],
|
||
'ethics' => [
|
||
'ethics_assessment' => "结合标题和摘要判断内容是否缺失伦理号或存在明显伦理问题[返回是/否]",
|
||
'ethics_explanation' => "逐个回答以下问题【1.[解释说明是否需要伦理号]2.[分析内容是否存在伦理号缺失]3.[是否存在明显伦理问题]】[返回格式字符串]",
|
||
],
|
||
'ai_registration' => [
|
||
'registration_assessment' => "结合标题和摘要判断是否需要临床注册号和知情同意书?[返回是/否]",
|
||
'registration_explanation' => "[结合标题和摘要解释说明是否需要临床注册号和知情同意书][返回格式字符串]",
|
||
],
|
||
'registration' => [
|
||
'registration_assessment' => "结合标题摘要判断内容是否缺失临床注册号和知情同意书[返回是/否]",
|
||
'registration_explanation' => "逐个回答以下问题【1.[解释说明是否需要临床注册号和知情同意书]2.[分析内容是否存在临床注册号和知情同意书]】[返回格式字符串]",
|
||
],
|
||
'academic' => [
|
||
'academic_assessment' => "是/否",
|
||
'academic_explanation' => "解释说明",
|
||
],
|
||
'contradiction' => [
|
||
'contradiction_assessment' => "是/否",
|
||
'contradiction_explanation' => "解释说明",
|
||
],
|
||
'fund_number' => [
|
||
'fund_number' => "1.[内容是否有基金号]2.[解释说明][返回格式字符串]"
|
||
],
|
||
'attribute' => [
|
||
'attribute_assessment' => "内容是否有科学性和创新性[包括但不限于科学性(结论是否科学、参考文献是否新颖等);创新性(结论与当前研究水平相比是否有明显突破、参考文献的时间)][返回是/否]",
|
||
'attribute_explanation' => "1.科学性[结论是否科学、参考文献是否新颖等]2.创新性[结论与当前研究水平相比是否有明显突破、参考文献的时间][返回格式字符串]",
|
||
],
|
||
'hotspot' => [
|
||
'hotspot' => "[逐个判断领域是否为热点领域并解释说明[返回格式:领域[解释说明],多个;分隔]]"
|
||
],
|
||
'reference' => [
|
||
'references_num' => "统计文章参考文献的数量[返回格式数量]",
|
||
'references_past_three' => "统计内容里近3年的参考文献的数量及所占比例[返回格式数量/占比]",
|
||
'references_past_five' => "统计内容里近5年的参考文献的数量及所占比例[返回格式数量/占比]",
|
||
'references_ratio_JCR1' => "根据2024JCR最新分区统计属于JCR 1区的数量及比例[返回格式数量/占比]",
|
||
'references_ratio_JCR2' => "根据2024JCR最新分区统计属于JCR 2区的数量及比例[返回格式数量/占比]",
|
||
]
|
||
];
|
||
|
||
//定义redis连接
|
||
private $oQueueRedis;
|
||
private $oHelperFunction;
|
||
public function __construct()
|
||
{
|
||
$this->oQueueRedis = QueueRedis::getInstance();
|
||
$this->oHelperFunction = new HelperFunction;
|
||
}
|
||
|
||
/**
|
||
* 微信公众号-构建公微模版[处理提示词]
|
||
*/
|
||
public function buildWechatPrompt($aSearch = []){
|
||
|
||
if(empty($aSearch)){
|
||
return [];
|
||
}
|
||
//文章类型-选择模版
|
||
$prompt_article_type = empty($aSearch['prompt_article_type']) ? 'default' : $aSearch['prompt_article_type'];
|
||
|
||
//判断内容是否为空
|
||
$aMainContent = empty($aSearch['main_content']) ? [] : $aSearch['main_content'];
|
||
if(empty($aMainContent)){
|
||
return [];
|
||
}
|
||
|
||
//处理文章内容
|
||
$aMainListResult = $this->dealArticleContent($aMainContent);
|
||
$aMainList = empty($aMainListResult['main_list']) ? [] : $aMainListResult['main_list'];//文字列表
|
||
$aMainImageList = empty($aMainListResult['main_image_list']) ? [] : $aMainListResult['main_image_list'];//图片列表
|
||
if(empty($aMainList)){
|
||
return [];
|
||
}
|
||
|
||
//获取问题
|
||
$aQuestion = $this->aWechatQuestion;
|
||
//系统角色
|
||
$sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message'];
|
||
if(empty($sSysMessagePrompt)){
|
||
return [];
|
||
}
|
||
|
||
//公共问题
|
||
$aMessage = [];
|
||
$aPublicQuestion = empty($aQuestion['public_message']) ? [] : $aQuestion['public_message'];
|
||
if(!empty($aPublicQuestion)){
|
||
foreach ($aPublicQuestion as $key => $value) {
|
||
//系统问题信息
|
||
$sInfo = json_encode([$key => $value],JSON_UNESCAPED_UNICODE);
|
||
$sSysMessagePromptInfo = $sSysMessagePrompt.$sInfo;
|
||
//用户输入内容
|
||
if($key == 'title_chinese'){
|
||
$sUserPrompt = empty($aSearch['{#title_chinese#}']) ? '' : $aSearch['{#title_chinese#}'];
|
||
}
|
||
if($key == 'covered'){
|
||
$sUserPrompt = empty($aSearch['{#title_chinese#}']) ? '' : $aSearch['{#title_chinese#}'];
|
||
$sUserPrompt .= empty($aSearch['{#abstract#}']) ? '' : $aSearch['{#abstract#}'];
|
||
}
|
||
$aMessage[] = [
|
||
['role' => 'system', 'content' => $sSysMessagePromptInfo],
|
||
['role' => 'user', 'content' => empty($sUserPrompt) ? '' : $sUserPrompt]
|
||
];
|
||
}
|
||
}
|
||
if(in_array($prompt_article_type, ['Mini Review','Review'])){
|
||
$aArticleQuestion = $this->dealReviewQuestion($aMainList,$aMainImageList);
|
||
}else{
|
||
$aArticleQuestion = $this->dealDefaultQuestion($aMainList,$aMainImageList);
|
||
}
|
||
$aMessage = array_merge($aMessage,$aArticleQuestion);
|
||
return $aMessage;
|
||
}
|
||
/**
|
||
* 微信公众号-处理文章内容
|
||
*/
|
||
private function dealArticleContent($aParam = []){
|
||
//内容
|
||
$aArticleMain = empty($aParam['main']) ? [] : $aParam['main'];
|
||
//内容-一级标题
|
||
$aArticleMainH1 = empty($aParam['main_h1']) ? [] : $aParam['main_h1'];
|
||
//处理数据
|
||
$aMainList = $aMainImageList = [];
|
||
if(empty($aArticleMain) || empty($aArticleMainH1)){
|
||
return $aMainList;
|
||
}
|
||
//数据处理
|
||
foreach ($aArticleMain as $key => $value) {
|
||
$sValue = trim($value['content']);
|
||
// 将HTML实体转换为Unicode字符
|
||
$sValue = html_entity_decode($sValue, ENT_QUOTES, 'UTF-8');
|
||
// 过滤掉所有空格字符(包括普通空格和NBSP)
|
||
$sValue = preg_replace('/\s+/', ' ', $sValue);
|
||
if(empty($sValue) || $sValue == " "){
|
||
continue;
|
||
}
|
||
$sKey = $this->oHelperFunction->isIdInRange($value['sort'],$aArticleMainH1);
|
||
if(!empty($sKey)){
|
||
$sKey = strtolower(str_replace(' ', '_', $this->oHelperFunction->filterAllTags($sKey)));
|
||
$value['content'] = $this->oHelperFunction->filterAllTags($value['content']);
|
||
if($value['type'] == 1){
|
||
$aMainImageList[$sKey][] = $value['ami_id'];
|
||
}
|
||
$aMainList[$sKey][] = $value;
|
||
}else{
|
||
$value['content'] = $this->oHelperFunction->filterAllTags($value['content']);
|
||
$aMainList['digest'][] = $value;
|
||
}
|
||
}
|
||
return ['main_list' => $aMainList,'main_image_list' => $aMainImageList];
|
||
}
|
||
/**
|
||
* 处理文章内容-研究结果
|
||
*/
|
||
private function dealResearchResult($aParam = []){
|
||
if(empty($aParam)){
|
||
return [];
|
||
}
|
||
// 处理数据
|
||
$result = [];
|
||
$currentH2 = null; // 当前h2信息
|
||
$currentContent = $currentImageContent = $h2Positions = []; // 中间内容 // 存储h2的位置信息 [am_id, 索引]
|
||
foreach ($aParam as $item) {
|
||
if ($item['is_h2'] == 1) {
|
||
// 记录当前h2位置
|
||
$h2Positions[] = $item['am_id'];
|
||
// 保存上一个h2数据
|
||
if ($currentH2) {
|
||
// 下一个h2的am_id就是当前刚记录的(因为数组是顺序添加的)
|
||
$nextAmId = count($h2Positions) > 1 ? end($h2Positions) : null;
|
||
$result[$currentH2['content']] = [
|
||
'current_am_id' => $currentH2['am_id'],
|
||
'next_am_id' => $nextAmId,
|
||
'content_between' => $currentContent,
|
||
'image_content_between' => $currentImageContent
|
||
];
|
||
}
|
||
// 更新当前h2
|
||
$currentH2 = $item;
|
||
$currentContent = [];
|
||
$currentImageContent = [];
|
||
}elseif ($currentH2) {// 收集中间内容
|
||
if($item['type'] == 0){
|
||
$currentContent[] = $item['content'];
|
||
}
|
||
if($item['type'] == 1){
|
||
$currentImageContent[] = $item['ami_id'];
|
||
}
|
||
}
|
||
}
|
||
// 处理最后一个h2
|
||
if ($currentH2) {
|
||
$result[$currentH2['content']] = [
|
||
'current_am_id' => $currentH2['am_id'],
|
||
'next_am_id' => 0,
|
||
'content_between' => $currentContent,
|
||
'image_content_between' => $currentImageContent
|
||
];
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 微信公众号-组装问题【默认】
|
||
*/
|
||
private function dealDefaultQuestion($aParam = [],$aMainImageList = []){
|
||
|
||
if(empty($aParam) || empty($aMainImageList)){
|
||
return [];
|
||
}
|
||
|
||
//获取问题
|
||
$aQuestion = $this->aWechatQuestion;
|
||
$aQuestionLists = empty($aQuestion['default']) ? [] : $aQuestion['default'];
|
||
if(empty($aQuestionLists)){
|
||
return [];
|
||
}
|
||
//系统角色
|
||
$sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message'];
|
||
if(empty($sSysMessagePrompt)){
|
||
return [];
|
||
}
|
||
|
||
//定义空问题
|
||
$aMessage = [];
|
||
|
||
//特殊标题强制转译
|
||
$aField = ['introduction' => 'research_background','background' => 'research_background','methods' => 'research_method','materials_and_methods' => 'research_method'];
|
||
|
||
//组装问题
|
||
foreach ($aParam as $key => $value) {
|
||
//字段处理
|
||
$key = trim($key,'_');
|
||
$key = empty($aField[$key]) ? $key : $aField[$key];
|
||
$sQuestionInfo = empty($aQuestionLists[$key]) ? '' : $aQuestionLists[$key];
|
||
if(empty($sQuestionInfo) || empty($value)){
|
||
continue;
|
||
}
|
||
|
||
//系统角色
|
||
$sInfo = json_encode([$key => $sQuestionInfo],JSON_UNESCAPED_UNICODE);
|
||
$sSysMessagePromptInfo = $sSysMessagePrompt.$sInfo;
|
||
|
||
//用户角色
|
||
$sUserPrompt = is_array($value) ? implode('', array_column($value, 'content')) : $value ;
|
||
if($key == 'results'){
|
||
$aResearchResult = $this->dealResearchResult($value);
|
||
if(!empty($aResearchResult)){
|
||
foreach ($aResearchResult as $k => $val) {
|
||
$sContentBetween = empty($val['content_between']) ? '' : implode(',', $val['content_between']);
|
||
if(empty($sContentBetween)){
|
||
continue;
|
||
}
|
||
|
||
//用户角色
|
||
$sUserPrompt = ['title' => $this->oHelperFunction->filterAllTags($k),'content' => $sContentBetween];
|
||
$aUserPrompt = ['results' =>['title' => '[将标题直接翻译中文,无需针对内容总结翻译]','content' => '['.$sQuestionInfo.']']];
|
||
|
||
//系统角色
|
||
$sSysMessagePromptInfo = $sSysMessagePrompt.json_encode($aUserPrompt,JSON_UNESCAPED_UNICODE);
|
||
|
||
//组装message
|
||
$aMessage[] = [
|
||
['role' => 'system', 'content' => $sSysMessagePromptInfo],
|
||
['role' => 'user', 'content' => empty($sUserPrompt) ? '' : json_encode($sUserPrompt,JSON_UNESCAPED_UNICODE)],
|
||
'current_am_id' => empty($val['current_am_id']) ? 0 : $val['current_am_id'],
|
||
'next_am_id' => empty($val['next_am_id']) ? 0 : $val['next_am_id'],
|
||
'ami_id' => empty($val['image_content_between']) ? '' : implode(',', $val['image_content_between'])
|
||
];
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
//组装
|
||
$aMessage[] = [
|
||
['role' => 'system', 'content' => $sSysMessagePromptInfo],
|
||
['role' => 'user', 'content' => empty($sUserPrompt) ? '' : $sUserPrompt]
|
||
];
|
||
}
|
||
return $aMessage;
|
||
}
|
||
/**
|
||
* 微信公众号-组装问题【reviwew】
|
||
*/
|
||
private function dealReviewQuestion($aParam = [],$aMainImageList = []){
|
||
|
||
if(empty($aParam) || empty($aMainImageList)){
|
||
return [];
|
||
}
|
||
|
||
//获取问题
|
||
$aQuestion = $this->aWechatQuestion;
|
||
$aQuestionLists = empty($aQuestion['default']) ? [] : $aQuestion['default'];
|
||
if(empty($aQuestionLists)){
|
||
return [];
|
||
}
|
||
//系统角色
|
||
$sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message'];
|
||
if(empty($sSysMessagePrompt)){
|
||
return [];
|
||
}
|
||
|
||
//定义空问题
|
||
$aMessage = [];
|
||
|
||
//特殊标题强制转译
|
||
$aField = ['introduction' => 'research_background','background' => 'research_background','methods' => 'research_method','materials_and_methods' => 'research_method'];
|
||
|
||
//组装问题
|
||
foreach ($aParam as $key => $value) {
|
||
//判空处理
|
||
if(empty($value)){
|
||
continue;
|
||
}
|
||
|
||
//一级标题处理
|
||
$key = trim($key,'_');
|
||
$sKey = str_replace('_', ' ', $key);
|
||
|
||
//字段处理
|
||
$key = empty($aField[$key]) ? $key : $aField[$key];
|
||
//系统问题信息
|
||
$sQuestionInfo = empty($aQuestionLists[$key]) ? '[针对content内容进行简单总结,采用连贯的段落形式,注意内容不要和文章内容有严重重复,总字数超过450字]' : $aQuestionLists[$key];
|
||
$sInfo = json_encode([$key => $sQuestionInfo],JSON_UNESCAPED_UNICODE);
|
||
$sSysMessagePromptInfo = $sSysMessagePrompt.$sInfo;
|
||
|
||
//用户输入内容处理
|
||
$sUserPrompt = is_array($value) ? implode('', array_column($value, 'content')) : $value ;
|
||
if(!in_array($key, ['background','research_background','introduction','conclusion','digest'])){
|
||
|
||
//系统角色
|
||
$aSysPrompt = ['results' =>['title' => '[将标题直接翻译中文,无需针对内容总结翻译]','content' => '['.$sQuestionInfo.']']];
|
||
$sSysMessagePromptInfo = $sSysMessagePrompt.json_encode($aSysPrompt,JSON_UNESCAPED_UNICODE);
|
||
|
||
//用户角色
|
||
$aUserPrompt = ['title' => $sKey,'content' => is_array($value) ? implode('', array_column($value, 'content')) : $value];
|
||
$sUserPrompt = json_encode($aUserPrompt,JSON_UNESCAPED_UNICODE);
|
||
}
|
||
//组装问题
|
||
$aMessage[] = [
|
||
['role' => 'system', 'content' => $sSysMessagePromptInfo],
|
||
['role' => 'user', 'content' => empty($sUserPrompt) ? '' : $sUserPrompt],
|
||
'current_am_id' => empty($value[0]['am_id']) ? 0 : $value[0]['am_id'],
|
||
'ami_id' => empty($aMainImageList[$key]) ? '' : implode(',', $aMainImageList[$key]),
|
||
];
|
||
}
|
||
return $aMessage;
|
||
}
|
||
/**
|
||
* 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;
|
||
|
||
// 降低随机性(0-1,0为最确定)
|
||
$iTemperature = empty($aParam['temperature']) ? '0.1' : $aParam['temperature'];
|
||
|
||
//组装数据
|
||
$data = [
|
||
'model' => $model,
|
||
'messages' => $aMessage,
|
||
'temperature' => $iTemperature,
|
||
];
|
||
$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->oHelperFunction->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']) ? 'deepseek-chat' : $aParam['model']; // 明确指定DeepSeek模型
|
||
$timeout = empty($aParam['timeout']) ? 600 : $aParam['timeout']; // 进一步延长超时到15分钟
|
||
$sUrl = empty($aParam['url']) ? $this->sUrl : $aParam['url'];
|
||
$iTemperature = empty($aParam['temperature']) ? '0.2' : $aParam['temperature'];
|
||
$iTop = empty($aParam['top_p']) ? '0.9' : $aParam['top_p'];
|
||
// 组装数据 - 增加流式传输必要参数
|
||
$data = [
|
||
'model' => $model,
|
||
'messages' => $aMessage,
|
||
'temperature' => $iTemperature,
|
||
'top_p' => $iTop,
|
||
'frequency_penalty' => 0.3,
|
||
'presence_penalty' => 0.2,
|
||
'stream' => true,
|
||
'stream_options' => ['include_usage' => false] // 减少额外数据传输
|
||
];
|
||
|
||
// Curl配置 - 增强流式传输兼容性
|
||
$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,
|
||
'Accept: text/event-stream',
|
||
'Cache-Control: no-cache',
|
||
'Connection: keep-alive', // 保持长连接
|
||
// 'Content-Length: ' . strlen(json_encode($data)), // 显式指定内容长度
|
||
// 'Expect: ' // 取消"100 Continue"预期,避免服务器等待确认
|
||
]);
|
||
|
||
// 代理与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_SSLVERSION, CURL_SSLVERSION_TLSv1_2); // 强制TLS版本
|
||
|
||
// 核心传输配置 - 解决数据截断
|
||
curl_setopt($this->curl, CURLOPT_POST, true);
|
||
curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($data));
|
||
curl_setopt($this->curl, CURLOPT_TIMEOUT, $timeout);
|
||
curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, 30);
|
||
// 设置超时时间(单位:秒),根据业务场景调整
|
||
curl_setopt($this->curl, CURLOPT_NOSIGNAL, true); // 禁用信号处理,解决超时精度问题
|
||
curl_setopt($this->curl, CURLOPT_BUFFERSIZE, 32); // 更小的缓冲区,更及时的处理
|
||
curl_setopt($this->curl, CURLOPT_FRESH_CONNECT, true);
|
||
curl_setopt($this->curl, CURLOPT_FORBID_REUSE, true); // 禁止重用连接
|
||
|
||
// 流式响应处理 - 增加分块校验
|
||
$streamContent = '';
|
||
$isComplete = false;
|
||
$incompleteLine = '';
|
||
$lastReceivedTime = time();
|
||
$receivedChunks = 0; // 跟踪接收的分块数量
|
||
|
||
// 改进的回调函数 - 实时处理并校验每一块数据
|
||
curl_setopt($this->curl, CURLOPT_WRITEFUNCTION, function ($curl, $data) use (
|
||
&$streamContent, &$isComplete, &$incompleteLine, &$lastReceivedTime, &$receivedChunks
|
||
) {
|
||
$lastReceivedTime = time();
|
||
$receivedChunks++;
|
||
|
||
// 处理跨块的不完整行(核心修复点)
|
||
$fullData = $incompleteLine . $data;
|
||
$lineEndPos = strrpos($fullData, "\n");
|
||
|
||
if ($lineEndPos !== false) {
|
||
// 提取完整行
|
||
$completePart = substr($fullData, 0, $lineEndPos + 1);
|
||
$incompleteLine = substr($fullData, $lineEndPos + 1);
|
||
|
||
$streamContent .= $completePart;
|
||
|
||
// 检查是否包含结束标记
|
||
if (strpos($completePart, 'data: [DONE]') !== false) {
|
||
$isComplete = true;
|
||
}
|
||
} else {
|
||
// 没有完整行,全部暂存
|
||
$incompleteLine = $fullData;
|
||
}
|
||
|
||
return strlen($data); // 必须返回完整长度,否则curl会中断
|
||
});
|
||
// 执行请求
|
||
$result = curl_exec($this->curl);
|
||
$curlErrno = curl_errno($this->curl);
|
||
$httpCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
|
||
$totalReceived = curl_getinfo($this->curl, CURLINFO_SIZE_DOWNLOAD);
|
||
// 其他错误
|
||
$error = '接口错误';
|
||
if(curl_error($this->curl) !== 0) {
|
||
$error = curl_error($this->curl); // 正确使用有效句柄
|
||
$httpCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
|
||
$error = "AI审稿传输失败:{$error},HTTP状态码:{$httpCode}";
|
||
}
|
||
curl_close($this->curl);
|
||
|
||
// 处理最后剩余的不完整数据(关键修复)
|
||
if (!empty($incompleteLine)) {
|
||
$streamContent .= $incompleteLine;
|
||
if (strpos($incompleteLine, 'data: [DONE]') !== false) {
|
||
$isComplete = true;
|
||
}
|
||
}
|
||
|
||
// 错误处理 - 区分不同类型的不完整情况
|
||
if (!empty($curlErrno)) {
|
||
// 超时但已完成
|
||
if ($curlErrno == CURLE_OPERATION_TIMEDOUT && $isComplete) {
|
||
$sStreamResponse = $this->oHelperFunction->parseMedicalStreamResponse($streamContent);
|
||
return json_encode([
|
||
'status' => 1,
|
||
'msg' => 'success',
|
||
'data' => $sStreamResponse,
|
||
'chunks' => $receivedChunks
|
||
]);
|
||
}
|
||
|
||
// 超时但有部分数据
|
||
if ($curlErrno == CURLE_OPERATION_TIMEDOUT) {
|
||
return json_encode([
|
||
'status' => 3,
|
||
'msg' => "超时,已接收{$receivedChunks}个分块",
|
||
'partial_data' => $this->oHelperFunction->parseMedicalStreamResponse($streamContent),
|
||
'is_complete' => $isComplete ? 1 : 0,
|
||
'chunks' => $receivedChunks,
|
||
'received_bytes' => $totalReceived
|
||
]);
|
||
}
|
||
return json_encode([
|
||
'status' => 4,
|
||
'msg' => "CURL错误: " . $error,
|
||
'error_code' => $curlErrno,
|
||
'http_code' => $httpCode
|
||
]);
|
||
}
|
||
|
||
// HTTP状态码检查
|
||
if ($httpCode < 200 || $httpCode >= 300) {
|
||
return json_encode([
|
||
'status' => 5,
|
||
'msg' => "HTTP错误: {$httpCode}",
|
||
'response' => $streamContent,
|
||
'chunks' => $receivedChunks
|
||
]);
|
||
}
|
||
|
||
// 处理正常结果
|
||
$sStreamResponse = $this->oHelperFunction->parseMedicalStreamResponse($streamContent);
|
||
return json_encode([
|
||
'status' => 1,
|
||
'msg' => 'success',
|
||
'data' => $sStreamResponse,
|
||
'is_complete' => $isComplete ? 1 : 0,
|
||
'chunks' => $receivedChunks,
|
||
'received_bytes' => $totalReceived
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 微信公众号-生成公微内容(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 = 'queue_job:ai_create_article:'.$iId;
|
||
$result = $this->oQueueRedis->recordQuestionProcessingStart($sRedisKey,$iNum);
|
||
$result = empty($result) ? 0 : $result;
|
||
if($result == 1){
|
||
//定义空数组
|
||
foreach ($aMessage as $key => $value) {
|
||
$aParam['current_am_id'] = empty($value['current_am_id']) ? 0 : $value['current_am_id'];
|
||
$aParam['next_am_id'] = empty($value['next_am_id']) ? 0 : $value['next_am_id'];
|
||
$aParam['ami_id'] = empty($value['ami_id']) ? 0 : $value['ami_id'];
|
||
if(isset($aParam['current_am_id'])){
|
||
unset($value['current_am_id']);
|
||
}
|
||
if(isset($aParam['next_am_id'])){
|
||
unset($value['next_am_id']);
|
||
}
|
||
if(isset($aParam['ami_id'])){
|
||
unset($value['ami_id']);
|
||
}
|
||
$aParam['messages'] = $value;
|
||
$aParam['chunkIndex'] = $key;
|
||
$aParam['count_num'] = $iNum;
|
||
// if($key%2 == 0){
|
||
$aParam['key_name'] = 'queue_1_completed';
|
||
Queue::push('app\api\job\createFieldForQueue@fire', $aParam, 'createFieldForQueue');
|
||
// }
|
||
|
||
// }else{
|
||
// $aParam['url'] = $this->sAiUrl;
|
||
// $aParam['key_name'] = 'queue_2_completed';
|
||
// Queue::push('app\api\job\createFieldForQueue@fire', $aParam, 'createFieldForQueueBak');
|
||
// }
|
||
|
||
}
|
||
return json_encode(['status' => 1, 'msg' => 'Content is being generated, please wait']);
|
||
}
|
||
if($result == 2){
|
||
return json_encode(['status' => 3, 'msg' => 'The data has been generated, please proceed with the next steps']);
|
||
}
|
||
if($result == 3){
|
||
return json_encode(['status' => 4, 'msg' => 'Content is being generated, please wait']);
|
||
}
|
||
return json_encode(['status' => 5, 'msg' => 'Redis write failure']);
|
||
}
|
||
/**
|
||
* 微信公众号-生成内容队列形式
|
||
*/
|
||
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']);
|
||
}
|
||
//请求OPENAI
|
||
$aParam['temperature'] = '0.6';
|
||
$aParam['top_p'] = '0.8';
|
||
$aResult = $this->curlOpenAIStream($aParam);
|
||
//更新处理进度
|
||
$iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex'];
|
||
$sRedisKey = 'queue_job:ai_create_article:'.$iId;
|
||
$sKeyName = empty($aParam['key_name']) ? 'queue_1_completed' : $aParam['key_name'];
|
||
$iProgress = $this->oQueueRedis->updateQuestionProcessingProgress($sRedisKey,$sKeyName);
|
||
//保存内容
|
||
$sRedisKey = 'queue_job: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->oHelperFunction->extractAndParse($aDataInfo);
|
||
if(!empty($aData) && is_string($aData)){
|
||
$aData = json_decode($aData,true);
|
||
}
|
||
$aData = empty($aData['data']) ? [] : $aData['data'];
|
||
$aData['current_am_id'] = empty($aParam['current_am_id']) ? 0 : $aParam['current_am_id'];
|
||
$aData['next_am_id'] = empty($aParam['next_am_id']) ? 0 : $aParam['next_am_id'];
|
||
$aData['ami_id'] = empty($aParam['ami_id']) ? 0 : $aParam['ami_id'];
|
||
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;
|
||
//判断是否生成
|
||
$is_generate = empty($aParam['is_generate']) ? 2 : $aParam['is_generate'];
|
||
if($is_generate == 1 || empty($aParam['results'])){
|
||
$aResult = json_decode($oArticle->updateAiArticle($aParam),true);
|
||
}
|
||
if(!empty($aParam['results'])){
|
||
$aData = $aParam['results'];
|
||
$aData['current_am_id'] = empty($aParam['current_am_id']) ? 0 : $aParam['current_am_id'];
|
||
$aData['next_am_id'] = empty($aParam['next_am_id']) ? 0 : $aParam['next_am_id'];
|
||
$aData['ami_id'] = empty($aParam['ami_id']) ? 0 : $aParam['ami_id'];
|
||
$aData['article_id'] = $iArticleId;
|
||
$aResult = json_decode($oArticle->updateAiArticleResults($aData),true);
|
||
}
|
||
$iStatus = empty($aResult['status']) ? 0 : $aResult['status'];
|
||
$sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg'];
|
||
//内容生成完成推送上传素材队列
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 构建AI审稿-处理提示词
|
||
*/
|
||
public function buildReviewPrompt($aSearch = [])
|
||
{
|
||
if(empty($aSearch)){
|
||
return [];
|
||
}
|
||
|
||
//问题等级
|
||
$sQuestionLevel = empty($aSearch['question_level']) ? '' : $aSearch['question_level'];
|
||
//提问字段
|
||
$aQuestionFields = empty($aSearch['queue_fields']) ? [] : explode(',', $aSearch['queue_fields']);
|
||
if(empty($aQuestionFields) || empty($sQuestionLevel)){
|
||
return [];
|
||
}
|
||
unset($aSearch['queue_fields']);
|
||
//获取问题
|
||
$aQuestion = $this->aReviewQuestion;
|
||
//系统角色
|
||
$sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message'];
|
||
if(empty($sSysMessagePrompt)){
|
||
return [];
|
||
}
|
||
$aQuestion = empty($aQuestion[$sQuestionLevel]) ? [] : $aQuestion[$sQuestionLevel];
|
||
if(empty($aQuestion)){
|
||
return [];
|
||
}
|
||
$aScopeReturn = $this->aScopeReturn;//返回json格式
|
||
//处理数据
|
||
$aMessage = [];
|
||
foreach ($aQuestion as $key => $value) {
|
||
if(!in_array($key, $aQuestionFields) || empty($value)){
|
||
continue;
|
||
}
|
||
//系统角色
|
||
$sSysMessageInfo = empty($aScopeReturn[$key]) ? '' : json_encode($aScopeReturn[$key],JSON_UNESCAPED_UNICODE);
|
||
$sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $value);
|
||
$aMessage[] = [
|
||
['role' => 'system', 'content' => $sSysMessagePrompt.$sSysMessageInfo],
|
||
['role' => 'user', 'content' => $sUserPrompt],
|
||
'field_name' => $key
|
||
];
|
||
|
||
}
|
||
return $aMessage;
|
||
}
|
||
/**
|
||
* AI审稿-队列处理
|
||
*/
|
||
public function articleReviewDeal($aParam = []){
|
||
//文章ID
|
||
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
|
||
if(empty($iArticleId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an article']);
|
||
}
|
||
//期刊ID
|
||
$iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id'];
|
||
if(empty($iJournalId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an Journal']);
|
||
}
|
||
//提问信息
|
||
$aMessage = empty($aParam['messages']) ? [] : $aParam['messages'];
|
||
if (empty($aMessage)) {
|
||
return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']);
|
||
}
|
||
//记录处理开始
|
||
foreach ($aMessage as $key => $value) {
|
||
$aParam['field_name'] = $value['field_name'];
|
||
unset($value['field_name']);
|
||
$aParam['messages'] = $value;
|
||
$aParam['chunkIndex'] = $aParam['field_name'];
|
||
$aParam['key_name'] = 'queue_1_completed';
|
||
Queue::push('app\api\job\ArticleReviewForQueue@fire', $aParam, 'ArticleReviewForQueue');
|
||
}
|
||
return json_encode(['status' => 1, 'msg' => 'AI review in progress, please wait']);
|
||
}
|
||
/**
|
||
* AI审稿-队列形式
|
||
*/
|
||
public function articleReviewForQueue($aParam = []){
|
||
|
||
//文章ID
|
||
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
|
||
if(empty($iArticleId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an article']);
|
||
}
|
||
//期刊ID
|
||
$iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id'];
|
||
if(empty($iJournalId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an Journal']);
|
||
}
|
||
|
||
//字段名
|
||
$sFieldsName = empty($aParam['field_name']) ? '' : $aParam['field_name'];
|
||
if(empty($sFieldsName)){
|
||
return json_encode(['status' => 3, 'msg' => 'Field is empty']);
|
||
}
|
||
|
||
//查询文章审核内容
|
||
$oAireview = new \app\common\Aireview;
|
||
$aAireview = $oAireview->get($aParam);
|
||
$aAireview = empty($aAireview['data']) ? [] : $aAireview['data'];
|
||
if(!empty($aAiReview['is_finish']) && $aAiReview['is_finish'] == 1){
|
||
return json_encode(array('status' => 1,'msg' => 'AI has been reviewed:'.$sFieldsName));
|
||
}
|
||
|
||
//提问信息
|
||
$aMessage = empty($aParam['messages']) ? [] : $aParam['messages'];
|
||
if (empty($aMessage)) {
|
||
return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']);
|
||
}
|
||
//问题等级
|
||
$sQuestionLevel = empty($aParam['question_level']) ? '' : $aParam['question_level'];
|
||
|
||
//判断是否执行过
|
||
$sRedisKey = 'queue_job:review_progress:'.$iArticleId.'_'.$iJournalId;
|
||
$sChunkProgress = $this->oQueueRedis->getChunkProgress($sRedisKey, $sFieldsName);
|
||
$aResult = $sChunkProgress;
|
||
if(empty($sChunkProgress)){
|
||
//请求OPENAI
|
||
$aParam['temperature'] = '0.1';
|
||
$aParam['top_p'] = '0.9';
|
||
$aResult = $this->curlOpenAIStream($aParam);
|
||
//保存内容
|
||
$this->oQueueRedis->saveChunkProgress($sRedisKey, $sFieldsName,$aResult);
|
||
}
|
||
//更新入库
|
||
$aReturnData = json_decode($aResult,true);
|
||
$aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data'];
|
||
$aData = empty($aDataInfo) ? [] : $this->oHelperFunction->extractAndParse($aDataInfo,$sChunkProgress);
|
||
if(!empty($aData) && is_string($aData)){
|
||
$aData = json_decode($aData,true);
|
||
}
|
||
$aData = empty($aData['data']) ? [] : $aData['data'];
|
||
if(is_object($aData)){
|
||
$aData = get_object_vars($aData);
|
||
}
|
||
if(!empty($aData)){//更新AI审稿记录表
|
||
$aData['article_id'] = $iArticleId;
|
||
$aData['journal_id'] = $iJournalId;
|
||
$aData['question_level'] = $sQuestionLevel;
|
||
$aData['field_name'] = $sFieldsName;
|
||
$result = $this->addAiReview($aData);
|
||
}
|
||
return $aResult;
|
||
}
|
||
/**
|
||
* 构建AI审稿-处理提示词[大文本]
|
||
*/
|
||
public function buildReviewPromptChunk($aSearch = []){
|
||
if(empty($aSearch)){
|
||
return [];
|
||
}
|
||
|
||
//问题等级
|
||
$sQuestionLevel = empty($aSearch['question_level']) ? '' : $aSearch['question_level'];
|
||
//提问字段
|
||
$sQuestionFields = empty($aSearch['queue_fields']) ? '' : $aSearch['queue_fields'];
|
||
if(empty($sQuestionFields) || empty($sQuestionLevel)){
|
||
return [];
|
||
}
|
||
unset($aSearch['queue_fields']);
|
||
//获取问题
|
||
$aQuestion = $this->aReviewQuestion;
|
||
//系统角色
|
||
$sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message'];
|
||
if(empty($sSysMessagePrompt)){
|
||
return [];
|
||
}
|
||
$aQuestion = empty($aQuestion[$sQuestionLevel]) ? [] : $aQuestion[$sQuestionLevel];
|
||
if(empty($aQuestion)){
|
||
return [];
|
||
}
|
||
|
||
$aScopeReturn = $this->aScopeReturn;//返回json格式
|
||
//处理数据
|
||
$aMessage = [];
|
||
|
||
//系统角色
|
||
$sSysMessageInfo = empty($aScopeReturn[$sQuestionFields]) ? '' : json_encode($aScopeReturn[$sQuestionFields],JSON_UNESCAPED_UNICODE);
|
||
if(empty($sSysMessageInfo)){
|
||
return [];
|
||
}
|
||
|
||
//处理数据
|
||
$aContent = empty($aSearch['content']) ? '' : $aSearch['content'];
|
||
if(empty($aContent)){
|
||
return [];
|
||
}
|
||
foreach ($aContent as $key => $value) {
|
||
$aSearch['content'] = $value;
|
||
$sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $aQuestion[$sQuestionFields]);
|
||
$aMessage[] = [
|
||
['role' => 'system', 'content' => $sSysMessagePrompt.$sSysMessageInfo],
|
||
['role' => 'user', 'content' => $sUserPrompt],
|
||
];
|
||
}
|
||
return $aMessage;
|
||
}
|
||
/**
|
||
* 微信公众号-生成公微内容(CURL)
|
||
*/
|
||
public function articleReviewDealChunk($aParam = []){
|
||
|
||
//文章ID
|
||
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
|
||
if(empty($iArticleId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an article']);
|
||
}
|
||
//期刊ID
|
||
$iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id'];
|
||
if(empty($iJournalId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an Journal']);
|
||
}
|
||
//提问信息
|
||
$aMessage = empty($aParam['messages']) ? [] : $aParam['messages'];
|
||
if (empty($aMessage)) {
|
||
return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']);
|
||
}
|
||
//提问字段
|
||
$sQuestionFields = empty($aParam['queue_fields']) ? '' : $aParam['queue_fields'];
|
||
if (empty($sQuestionFields)) {
|
||
return json_encode(['status' => 3, 'msg' => 'AI Q&A content not obtained']);
|
||
}
|
||
|
||
//记录处理开始
|
||
$iNum = count($aMessage);
|
||
$sRedisKey = 'queue_job:review_chunk:'.$iArticleId.'_'.$iJournalId.'_'.$sQuestionFields;
|
||
$result = $this->oQueueRedis->recordQuestionProcessingStart($sRedisKey,$iNum);
|
||
$result = empty($result) ? 0 : $result;
|
||
if($result == 1){
|
||
//定义空数组
|
||
foreach ($aMessage as $key => $value) {
|
||
$aParam['messages'] = $value;
|
||
$aParam['chunkIndex'] = $key;
|
||
$aParam['key_name'] = 'queue_1_completed';
|
||
Queue::push('app\api\job\ArticleReviewForQueueChunk@fire', $aParam, 'ArticleReviewForQueueChunk');
|
||
}
|
||
return json_encode(['status' => 1, 'msg' => 'AI review in progress, please wait']);
|
||
}
|
||
if($result == 2){
|
||
return json_encode(['status' => 3, 'msg' => 'The data has been generated, please proceed with the next steps']);
|
||
}
|
||
if($result == 3){
|
||
return json_encode(['status' => 4, 'msg' => 'AI review in progress, please wait']);
|
||
}
|
||
return json_encode(['status' => 5, 'msg' => 'Redis write failure']);
|
||
}
|
||
/**
|
||
* 微信公众号-审稿大文本分片传输队列
|
||
*/
|
||
public function articleReviewForQueueChunk($aParam = []){
|
||
|
||
//文章ID
|
||
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
|
||
if(empty($iArticleId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an article']);
|
||
}
|
||
//期刊ID
|
||
$iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id'];
|
||
if(empty($iJournalId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an Journal']);
|
||
}
|
||
|
||
//字段名
|
||
$sFieldsName = empty($aParam['queue_fields']) ? '' : $aParam['queue_fields'];
|
||
if(empty($sFieldsName)){
|
||
return json_encode(['status' => 3, 'msg' => 'Field is empty']);
|
||
}
|
||
|
||
//查询文章审核内容
|
||
$oAireview = new \app\common\Aireview;
|
||
$aAireview = $oAireview->get($aParam);
|
||
$aAireview = empty($aAireview['data']) ? [] : $aAireview['data'];
|
||
if(!empty($aAiReview['is_finish']) && $aAiReview['is_finish'] == 1){
|
||
return json_encode(array('status' => 1,'msg' => 'AI has been reviewed:'.$sFieldsName));
|
||
}
|
||
|
||
//提问信息
|
||
$aMessage = empty($aParam['messages']) ? [] : $aParam['messages'];
|
||
if (empty($aMessage)) {
|
||
return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']);
|
||
}
|
||
//问题等级
|
||
$sQuestionLevel = empty($aParam['question_level']) ? '' : $aParam['question_level'];
|
||
|
||
//请求OPENAI
|
||
$aParam['temperature'] = '0.1';
|
||
$aParam['top_p'] = '0.9';
|
||
$aResult = $this->curlOpenAIStream($aParam);
|
||
|
||
//更新处理进度
|
||
$iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex'];
|
||
$sRedisKey = 'queue_job:review_chunk:'.$iArticleId.'_'.$iJournalId.'_'.$sFieldsName;
|
||
$sKeyName = empty($aParam['key_name']) ? 'queue_1_completed' : $aParam['key_name'];
|
||
$iProgress = $this->oQueueRedis->updateQuestionProcessingProgress($sRedisKey,$sKeyName);
|
||
//保存内容
|
||
$sRedisKey = 'queue_job:review_progress_chunk:'.$iArticleId.'_'.$iJournalId.'_'.$sFieldsName;
|
||
$this->oQueueRedis->saveChunkProgress($sRedisKey, $iIndex,$aResult);
|
||
|
||
//判断是否执行完成
|
||
if($iProgress >= 100){
|
||
$aChunkProgress = $this->oQueueRedis->getChunkProgress($sRedisKey);
|
||
$aChunkProgress = $this->dealChunkProgres($aChunkProgress,$sFieldsName);
|
||
if(!empty($aChunkProgress)){
|
||
//判断是否执行过
|
||
$sRedisKey = 'queue_job:review_progress:'.$iArticleId.'_'.$iJournalId;
|
||
$this->oQueueRedis->saveChunkProgress($sRedisKey, $sFieldsName,json_encode($aChunkProgress));
|
||
//入库数据
|
||
$aChunkProgress['article_id'] = $iArticleId;
|
||
$aChunkProgress['journal_id'] = $iJournalId;
|
||
$aChunkProgress['question_level'] = $sQuestionLevel;
|
||
$aChunkProgress['field_name'] = $sFieldsName;
|
||
$result = $this->addAiReview($aChunkProgress);
|
||
}
|
||
}
|
||
return $aResult;
|
||
}
|
||
/**
|
||
* 微信公众号-审稿大文本分片传输[内容整合处理]
|
||
*/
|
||
private function dealChunkProgres($aParam = [],$sFieldsName = ''){
|
||
if(empty($aParam) || empty($sFieldsName)){
|
||
return [];
|
||
}
|
||
foreach ($aParam as $key => $value) {
|
||
//更新入库
|
||
$aReturnData = json_decode($value,true);
|
||
$aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data'];
|
||
$aData = empty($aDataInfo) ? [] : $this->oHelperFunction->extractAndParse($aDataInfo);
|
||
if(!empty($aData) && is_string($aData)){
|
||
$aData = json_decode($aData,true);
|
||
}
|
||
$aData = empty($aData['data']) ? [] : $aData['data'];
|
||
if(is_object($aData)){
|
||
$aData = get_object_vars($aData);
|
||
}
|
||
if(empty($aData)){
|
||
continue;
|
||
}
|
||
$aChunkData[$key] = $aData;
|
||
}
|
||
if(empty($aChunkData)){
|
||
return [];
|
||
}
|
||
//处理结果
|
||
return $this->aggregateYesNoDimension($aChunkData,$sFieldsName);
|
||
}
|
||
/**
|
||
* 微信公众号-审稿大文本分片传输[内容整合处理]
|
||
*/
|
||
private function aggregateYesNoDimension($aParam = [],$sFieldsName = '')
|
||
{
|
||
$aAssessments = $aExplanations = [];
|
||
foreach ($aParam as $key => $value) {
|
||
$sKey = $sFieldsName.'_assessment';
|
||
if(!isset($value[$sKey])){
|
||
$sKey = $sFieldsName;
|
||
}
|
||
if(empty($value[$sKey])){
|
||
continue;
|
||
}
|
||
$aAssessments[] = $value[$sKey];
|
||
$sKey = $sFieldsName.'_explanation';
|
||
if(isset($value[$sKey])){
|
||
if(empty($value[$sKey])){
|
||
continue;
|
||
}
|
||
$aExplanations[] = $value[$sKey];
|
||
}
|
||
}
|
||
if(!empty($aAssessments) && empty($aExplanations)){
|
||
return [
|
||
$sFieldsName => implode("\n", array_unique($aAssessments))
|
||
];
|
||
}
|
||
if(!empty($aAssessments) && !empty($aExplanations)){
|
||
return [
|
||
$sFieldsName.'_assessment' => in_array('是', $aAssessments) ? '是' : (empty($aAssessments) ? '无' : '否'),
|
||
$sFieldsName.'_explanation' => implode("\n", array_unique($aExplanations))
|
||
];
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* AI审稿-处理返回数据及入库
|
||
*/
|
||
private function addAiReview($aData = [],$sChunkProgress = ''){
|
||
if(empty($aData)){
|
||
return ['status' => 2,'msg' => '参数错误'];
|
||
}
|
||
//文章ID
|
||
$iArticleId = empty($aData['article_id']) ? 0 : $aData['article_id'];
|
||
if(empty($iArticleId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an article']);
|
||
}
|
||
//期刊ID
|
||
$iJournalId = empty($aData['journal_id']) ? 0 : $aData['journal_id'];
|
||
if(empty($iJournalId)){
|
||
return json_encode(['status' => 2, 'msg' => 'Please select an Journal']);
|
||
}
|
||
//问题等级
|
||
$sQuestionLevel = empty($aData['question_level']) ? '' : $aData['question_level'];
|
||
//字段名
|
||
$sFieldsName = empty($aData['field_name']) ? '' : $aData['field_name'];
|
||
if(empty($sFieldsName)){
|
||
return json_encode(['status' => 3, 'msg' => 'Field is empty']);
|
||
}
|
||
if($sFieldsName == 'journal_scope'){
|
||
$sKey = $sFieldsName.'_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '否'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => $sQuestionLevel,'queue_fields' => 'other_journal'];
|
||
}
|
||
if($sResult == '是'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ai_ethics'];
|
||
}
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}
|
||
if($sFieldsName == 'other_journal'){
|
||
$sKey = $sFieldsName.'_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '是'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ai_ethics'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aData['is_finish'] = 1;
|
||
}
|
||
}
|
||
if($sFieldsName == 'ai_ethics'){
|
||
$sKey = 'ethics_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '是'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ethics','explanation' => empty($aData['ethics_explanation']) ? 'AI返回为空' : $aData['ethics_explanation']];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ai_registration'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}
|
||
}
|
||
if($sFieldsName == 'ethics'){//伦理问题
|
||
$sKey = $sFieldsName.'_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '否'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ai_registration'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aData['is_finish'] = 1;
|
||
}
|
||
}
|
||
if($sFieldsName == 'ai_registration'){
|
||
$sKey = 'registration_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '是'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'registration','explanation' => empty($aData['registration_explanation']) ? 'AI返回为空' : $aData['registration_explanation']];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'academic'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}
|
||
}
|
||
if($sFieldsName == 'registration'){//临床注册号及知情同意书
|
||
$sKey = $sFieldsName.'_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '否'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'academic'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aData['is_finish'] = 1;
|
||
}
|
||
}
|
||
if($sFieldsName == 'academic'){//学术问题
|
||
$sKey = 'academic_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '否'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'contradiction'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aData['is_finish'] = 1;
|
||
}
|
||
}
|
||
if($sFieldsName == 'contradiction'){//矛盾问题
|
||
$sKey = 'contradiction_assessment';//是/否
|
||
$sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey];
|
||
if($sResult == '否'){
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'fund_number'];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}else{
|
||
$aData['is_finish'] = 1;
|
||
}
|
||
}
|
||
if($sFieldsName == 'fund_number'){//矛盾问题
|
||
// $sKey = 'fund_number';//是/否
|
||
// $sResult = empty($aData[$sKey]) ? '' : $aData[$sKey];
|
||
// if(!empty($sResult)){
|
||
//获取C级问题
|
||
$aQuestion = $this->aReviewQuestion;
|
||
$aQuestion = empty($aQuestion['C']) ? [] : $aQuestion['C'];
|
||
if(!empty($aQuestion)){
|
||
foreach ($aQuestion as $key => $value) {
|
||
$aQueueParam = ['article_id' => $iArticleId,'question_level' => 'C','queue_fields' => $key];
|
||
Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview');
|
||
}
|
||
}
|
||
// }
|
||
}
|
||
//审稿记录入库
|
||
if(empty($sChunkProgress)){
|
||
if($sQuestionLevel == 'C' && $sFieldsName = 'reference'){
|
||
$aData['is_finish'] = 1;
|
||
}
|
||
$oAireview = new \app\common\Aireview;
|
||
$result = $oAireview->addAiReview($aData);
|
||
}
|
||
return $result;
|
||
}
|
||
}
|