Files
tougao/application/common/OpenAi.php
2025-08-07 13:54:10 +08:00

1183 lines
54 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common;
use think\Cache;
use think\Db;
use think\Queue;
use app\common\Article;
use app\common\QueueRedis;
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 $aArticleImportantPrompt = [
"journal_scope" => [
'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}分析是否有明显的不合理之处?"
// ]
];
//公微问题模版
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审稿提示词
protected $aReviewQuestion = [
'system_message' => '您是一位资深的医学期刊学术评审专家,请负责严谨、客观地评估学术文章。请根据提供的医学论文信息,按照以下严格格式生成结构化[JSON结构]输出[中文]',
'default' => [
"journal_scope" => "结合标题和摘要以及期刊范围来判断文章是否符合目标期刊?",
"attribute" => "内容是否有科学前沿性和创新性?参照维度[研究内容的原创性:论文中的研究内容是否与已有的研究重复?是否在同样的领域提出了类似的结论,但在方法或结果上有所创新?如果有,作者是否清楚地解释了如何与之前的研究不同,或者如何在原有基础上进行扩展或改进?如果是综述文章,汇总并综合最新的研究成果,尤其是近几年内的重要发现,展示领域内最新的进展成果。作者可以识别出未被充分讨论的问题或提出新的研究问题,而不是简单文献堆砌。文章中的图表创新能否将信息的清晰呈现,方便读者理解复杂研究问题。论文方法创新性评估要点:是否采用了新的实验模型或创新的实验设计,能有效解决当前研究中的难点或空白?是否有合理的对照组和多组实验设计,确保研究结果的可靠性是否使用了当前前沿的技术如高通量测序、CRISPR基因编辑等,提高了实验精度或数据分析能力?是否结合了跨学科的方法(如生物信息学、人工智能等)?是否应用了多种验证手段或统计方法,确保结果的可信度?是否通过细胞实验、动物模型等多重验证,确保实验结果的可靠性?结论与数据的创新性:研究结论是否提出了新观点或新见解?是否提供了新的实验数据或观察结果,能够突破当前的研究局限?例如,发现了新的生物标志物,或对已知生物通路的作用机制提供了全新的解释]",
"contradiction" => "内容是否前后矛盾或存在逻辑不一致的问题?",
"unreasonable" => "内容是否有明显的不合理之处?",
"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" => "结合内容分析发表后被引用的概率"
]
];
//定义redis连接
private $oQueueRedis;
public function __construct()
{
$this->oQueueRedis = QueueRedis::getInstance();
}
/**
* 构建公微模版-处理提示词
*/
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->isIdInRange($value['sort'],$aArticleMainH1);
if(!empty($sKey)){
$sKey = strtolower(str_replace(' ', '_', strip_tags($sKey)));
$value['content'] = strip_tags($value['content']);
if($value['type'] == 1){
$aMainImageList[$sKey][] = $value['ami_id'];
continue;
}
$aMainList[$sKey][] = $value;
}else{
$value['content'] = strip_tags($value['content']);
$aMainList['digest'][] = $value;
}
}
return ['main_list' => $aMainList,'main_image_list' => $aMainImageList];
}
/**
* 判断ID是否在目标区间数组的范围内
* @param int $id 待判断的ID
* @param array $rangeMap 区间规则数组
* @return bool|string 存在则返回所属标题否则返回false
*/
private function isIdInRange($iSort,$rangeMap = []) {
foreach ($rangeMap as $title => $range) {
// 解析区间值(处理字符串和数字两种格式)
$rangeStr = is_string($range['range']) ? $range['range'] : (string)$range['range'];
$rangeParts = explode(',', $rangeStr);
// 单个值:表示 >= 该值
if (count($rangeParts) == 1) {
$min = (int)$rangeParts[0];
if ($iSort > $min) {
return $title; // 返回所属标题
}
}
// 两个值:表示 [min, max] 闭区间(包含两端)
elseif (count($rangeParts) == 2) {
$min = (int)$rangeParts[0];
$max = (int)$rangeParts[1];
if ($iSort >= $min && $iSort <= $max) {
return $title; // 返回所属标题
}
}
}
return ''; // 不在任何区间
}
/**
* 处理文章内容-研究结果
*/
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 = 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' => strip_tags($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;
}
//一级标题处理
$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;
}
/**
* 构建AI审稿-处理提示词
*/
public function buildReviewPrompt($aSearch = [])
{
if(empty($aSearch)){
return [];
}
//获取问题
$aQuestion = $this->aReviewQuestion;
$aQuestionLists = empty($aQuestion['default']) ? [] : $aQuestion['default'];
if(empty($aQuestionLists)){
return [];
}
//系统角色
$sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message'];
if(empty($sSysMessagePrompt)){
return [];
}
//问题处理
$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;
}
/**
* 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->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'];
// 组装数据 - 增加流式传输必要参数
$data = [
'model' => $model,
'messages' => $aMessage,
'temperature' => $iTemperature,
'top_p' => 0.8,
'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' // 保持长连接
]);
// 代理与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);
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->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->parseMedicalStreamResponse($streamContent),
'is_complete' => $isComplete ? 1 : 0,
'chunks' => $receivedChunks,
'received_bytes' => $totalReceived
]);
}
// 其他错误
return json_encode([
'status' => 4,
'msg' => "CURL错误: " . curl_error($this->curl),
'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->parseMedicalStreamResponse($streamContent);
return json_encode([
'status' => 1,
'msg' => 'success',
'data' => $sStreamResponse,
'is_complete' => $isComplete ? 1 : 0,
'chunks' => $receivedChunks,
'received_bytes' => $totalReceived
]);
}
/**
* 增强版流式响应解析 - 解决JSON片段拼接问题
*/
private function parseMedicalStreamResponse($streamContent){
$fullContent = '';
$lines = explode("\n", $streamContent);
$validLines = 0;
$errorLines = 0;
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// 处理DeepSeek的SSE格式
if (strpos($line, 'data: ') === 0) {
// 检查结束标记
if ($line === 'data: [DONE]') {
break;
}
$jsonStr = substr($line, 6);
$jsonData = json_decode($jsonStr, true);
// 解析错误处理与修复
if (json_last_error() !== JSON_ERROR_NONE) {
$errorLines++;
// 针对DeepSeek常见的JSON格式问题进行修复
$jsonStr = $this->fixDeepSeekJson($jsonStr);
$jsonData = json_decode($jsonStr, true);
}
// 提取内容兼容DeepSeek的响应结构
if (isset($jsonData['choices'][0]['delta']['content'])) {
$fullContent .= $jsonData['choices'][0]['delta']['content'];
$validLines++;
} elseif (isset($jsonData['choices'][0]['text'])) {
$fullContent .= $jsonData['choices'][0]['text'];
$validLines++;
}
}
}
// 记录解析统计,便于调试
error_log("流式解析: 有效行{$validLines}, 错误行{$errorLines}");
return $fullContent;
}
/**
* 高性能DeepSeek JSON修复函数终极版
* 确保修复后的JSON字符串100%可解析,同时保持最优性能
*/
private function fixDeepSeekJson($jsonStr) {
// 基础处理:去除首尾空白并处理空字符串(高效操作)
$jsonStr = trim($jsonStr);
if (empty($jsonStr)) {
return '{}';
}
// 1. 预处理:清除首尾干扰字符(减少正则使用)
$len = strlen($jsonStr);
$start = 0;
// 跳过开头的逗号和空白
while ($start < $len && ($jsonStr[$start] === ',' || ctype_space($jsonStr[$start]))) {
$start++;
}
$end = $len - 1;
// 跳过结尾的逗号和空白
while ($end >= $start && ($jsonStr[$end] === ',' || ctype_space($jsonStr[$end]))) {
$end--;
}
if ($start > 0 || $end < $len - 1) {
$jsonStr = substr($jsonStr, $start, $end - $start + 1);
$len = strlen($jsonStr);
// 处理截取后可能为空的情况
if ($len === 0) {
return '{}';
}
}
// 2. 括号平衡修复(核心逻辑保持,减少计算)
$braceDiff = substr_count($jsonStr, '{') - substr_count($jsonStr, '}');
if ($braceDiff !== 0) {
if ($braceDiff > 0) {
$jsonStr .= str_repeat('}', $braceDiff);
} else {
// 仅在必要时使用正则移除多余括号
$jsonStr = preg_replace('/}(?=([^"]*"[^"]*")*[^"]*$)/', '', $jsonStr, -$braceDiff);
}
}
$bracketDiff = substr_count($jsonStr, '[') - substr_count($jsonStr, ']');
if ($bracketDiff !== 0) {
if ($bracketDiff > 0) {
$jsonStr .= str_repeat(']', $bracketDiff);
} else {
$jsonStr = preg_replace('/](?=([^"]*"[^"]*")*[^"]*$)/', '', $jsonStr, -$bracketDiff);
}
}
// 3. 控制字符清理(合并为单次处理)
$jsonStr = preg_replace(
'/([\x00-\x1F\x7F]|[^\x20-\x7E\xA0-\xFF]|\\\\u001f|\\\\u0000)/',
'',
$jsonStr
);
// 4. 引号处理(仅在有引号时处理,减少操作)
if (strpos($jsonStr, '"') !== false) {
// 修复未转义引号(优化正则)
$jsonStr = preg_replace('/(?<!\\\\)"/', '\\"', $jsonStr);
// 修复过度转义(简化正则)
$jsonStr = str_replace('\\\\"', '\\"', $jsonStr);
// 闭合未结束的引号(高效计数)
if (substr_count($jsonStr, '"') % 2 !== 0) {
$jsonStr .= '"';
}
}
// 5. 空白字符规范化(字符串替换比正则更快)
$jsonStr = str_replace("\n", "\\n", $jsonStr);
$jsonStr = str_replace("\r", "", $jsonStr);
// 仅在有连续空白时处理
if (strpos($jsonStr, ' ') !== false) {
$jsonStr = preg_replace('/\s+/', ' ', $jsonStr);
}
// 6. 语法错误修复(精简正则)
$jsonStr = preg_replace(
['/([{,]\s*)([\w-]+)(\s*:)/', '/,\s*([}\]])/'],
['$1"$2"$3', ' $1'],
$jsonStr
);
// 7. 首尾完整性检查(高效判断)
$firstChar = $jsonStr[0];
if ($firstChar !== '{' && $firstChar !== '[') {
$jsonStr = '{' . $jsonStr . '}';
$firstChar = '{'; // 更新首字符
}
$lastPos = strlen($jsonStr) - 1;
$lastChar = $jsonStr[$lastPos];
if ($lastChar !== '}' && $lastChar !== ']') {
// 移除末尾无效字符
if (in_array($lastChar, [',', ' ', ':'])) {
$jsonStr = rtrim($jsonStr, $lastChar);
}
// 补全结尾
$jsonStr .= ($firstChar === '{') ? '}' : ']';
}
// 8. 最终验证与多级容错机制
$errorCode = json_last_error();
$attempts = 0;
$maxAttempts = 2;
// 多级修复尝试
while ($attempts < $maxAttempts) {
$test = json_decode($jsonStr);
$errorCode = json_last_error();
if ($errorCode === JSON_ERROR_NONE) {
return $jsonStr;
}
// 根据错误类型进行针对性修复
$jsonStr = $this->handleJsonError($jsonStr, $errorCode);
$attempts++;
}
// 终极容错如果所有尝试都失败返回空JSON对象
return '{}';
}
/**
* 根据JSON解析错误类型进行针对性修复
*/
private function handleJsonError($jsonStr, $errorCode) {
switch ($errorCode) {
case JSON_ERROR_SYNTAX:
// 语法错误:尝试更激进的清理
$jsonStr = preg_replace('/[^\w{}[\]":,.\s\\\]/', '', $jsonStr);
$jsonStr = preg_replace('/,\s*([}\]])/', ' $1', $jsonStr);
break;
case JSON_ERROR_CTRL_CHAR:
// 控制字符错误:进一步清理控制字符
$jsonStr = preg_replace('/[\x00-\x1F\x7F]/u', '', $jsonStr);
break;
case JSON_ERROR_UTF8:
// UTF8编码错误尝试重新编码
$jsonStr = utf8_encode(utf8_decode($jsonStr));
break;
default:
// 其他错误:使用备用修复策略
$jsonStr = $this->fallbackJsonFix($jsonStr);
}
return $jsonStr;
}
/**
* 备用JSON修复策略更激进的修复方式
* 当主修复逻辑失败时使用
*/
private function fallbackJsonFix($jsonStr) {
// 更彻底的清理
$jsonStr = preg_replace('/[^\w{}[\]":,.\s\\\]/u', '', $jsonStr);
if (!preg_match('/^[\[{]/', $jsonStr)) {
$jsonStr = '{' . $jsonStr . '}';
}
// 最后尝试平衡括号
$openBrace = substr_count($jsonStr, '{');
$closeBrace = substr_count($jsonStr, '}');
$jsonStr .= str_repeat('}', max(0, $openBrace - $closeBrace));
$openBracket = substr_count($jsonStr, '[');
$closeBracket = substr_count($jsonStr, ']');
$jsonStr .= str_repeat(']', max(0, $openBracket - $closeBracket));
// 确保结尾正确
$lastChar = substr($jsonStr, -1);
if ($lastChar !== '}' && $lastChar !== ']') {
$jsonStr .= preg_match('/^\{/', $jsonStr) ? '}' : ']';
}
return $jsonStr;
}
/**
* 微信公众号-生成公微内容(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';
$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->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);
}
/**
* 从文本中提取被```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]) ? $text : $matches[1];
// 若未提取到尝试宽松匹配允许没有json标记
if (empty($jsonContent)) {
preg_match('/```\s*(\{.*?\})\s*```/s', $text, $matches);
$jsonContent = empty($matches[1]) ? $text : $matches[1];
}
// 清理JSON内容去除多余标记和控制字符
$jsonContent = trim(trim($jsonContent, '```json'), '```');
$jsonContent = preg_replace('/[\x00-\x1F\x7F]/', '', $jsonContent); // 过滤所有控制字符
// 解析JSON
$aData = json_decode($jsonContent, $assoc);
// 检查解析结果
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'status' => 2,
'msg' => "API返回无效JSON: " . json_last_error_msg() . '===============' . $jsonContent,
'data' => null
];
}
return ['status' => 1, 'msg' => 'success', 'data' => $aData];
}
}