KeywordPageAiContent.php 10.2 KB
<?php

namespace App\Console\Commands\Tdk;

use App\Exceptions\ValidateException;
use App\Helper\Arr;
use App\Helper\Common;
use App\Helper\Gpt;
use App\Models\Ai\AiCommand;
use App\Models\Ai\AiTdkErrorLog;
use App\Models\Com\NoticeLog;
use App\Models\Domain\DomainInfo;
use App\Models\Product\Keyword;
use App\Models\Project\AggregateKeywordAffix;
use App\Models\Project\DeployBuild;
use App\Models\Project\DeployOptimize;
use App\Models\Project\ProjectKeywordAiTask;
use App\Services\ProjectServer;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

/**
 * 关键词聚合页AI生成内容
 * Class InitProject
 * @package App\Console\Commands
 * @author zbj
 * @date 2025/06/06
 */
class KeywordPageAiContent extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'keyword_page_ai_content';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '关键词聚合页AI生成内容';

    /**
     * 统计图表类型 随机一个
     * @var string[]
     */
    protected $chart_types = [
        '柱状图',
        '折线图',
    ];

    /**
     * @return bool
     */
    public function handle()
    {
        while (true) {
            $task = ProjectKeywordAiTask::getPendingTask();
            if (!$task) {
                sleep(10);
                continue;
            }
            $project_id = $task->project_id;

            echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' start project_id: ' . $project_id . PHP_EOL;
            try {
                ProjectServer::useProject($project_id);

                $update_rows = $this->ai_content($task);

                DB::disconnect('custom_mysql');

                ProjectKeywordAiTask::finish($task->id, $update_rows);

               // $update_rows && $this->sendNotify($project_id);

            } catch (ValidateException $e) {
                echo getmypid() . ' ' . date('Y-m-d H:i:s') . 'line: ' . $e->getLine() . ' error: ' . $project_id . '->' . $e->getMessage() . PHP_EOL;
                $task->status = ProjectKeywordAiTask::STATUS_FAIL;
                $task->remark = mb_substr($e->getMessage(), 0, 250);
                $task->save();
            } catch (\Exception $e) {
                echo getmypid() . ' ' . date('Y-m-d H:i:s') . 'line: ' . $e->getLine() . ' error: ' . $project_id . '->' . $e->getMessage() . PHP_EOL;
                ProjectKeywordAiTask::retry($task->id, $e->getMessage());
            }
            echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' end project_id: ' . $project_id . PHP_EOL;
        }
    }


    /**
     * @param ProjectKeywordAiTask $task
     * @author zbj
     * @date 2025/6/6
     */
    public function ai_content(ProjectKeywordAiTask $task)
    {
        //前后缀
        $affix = AggregateKeywordAffix::where('project_id', $task->project_id)->first();
        $prefix = empty($affix['prefix']) ? [] : array_map('strtolower', explode("\r\n", $affix['prefix']));
        $suffix = empty($affix['suffix']) ? [] : array_map('strtolower', explode("\r\n", $affix['suffix']));
        if (!$prefix || !$suffix) {
            throw new ValidateException('扩展标题前后缀不存在');
        }
        //公司英文描述
        $company_en_description = DeployOptimize::where('project_id', $task->project_id)->value('company_en_description');
        if (!$company_en_description) {
            throw new ValidateException('公司英文描述不存在');
        }
        //指令
        $ai_commands = AiCommand::whereIn('key', ['tag_sale_content', 'tag_count_content', 'tag_data_table'])->where('project_id', 0)->select('key', 'scene', 'ai')->get()->toArray();
        $project_ai_commands = AiCommand::whereIn('key', ['tag_sale_content', 'tag_count_content', 'tag_data_table'])->where('project_id', $task->project_id)->select('key', 'scene', 'ai')->get()->toArray();
        $ai_commands = Arr::setValueToKey($ai_commands, 'key');
        $project_ai_commands = Arr::setValueToKey($project_ai_commands, 'key');
        foreach ($ai_commands as $k => $ai_command) {
            if (!empty($project_ai_commands[$k])) {
                $ai_commands[$k] = $project_ai_commands[$k];
            }
        }

        //没有标题、文案、图表的关键词
        $keyword_ids = Keyword::whereNull('sale_title')->orWhereNull('sale_content')->orWhereNull('table_html')
            ->orWhereNull('count_title')->orWhereNull('count_html')
            ->pluck('id')
            ->toArray();
        $update_rows = 0;
        foreach ($keyword_ids as $id) {
            //缓存 在处理的项目数据 id
            $cache_key = "keyword_page_ai_content_task_lock_{$task->project_id}_{$id}";
            if (!Redis::setnx($cache_key, 1)) {
                continue;
            }
            Redis::expire($cache_key, 120);

            $keyword = Keyword::where('id', $id)->select(['id', 'title', 'sale_title', 'sale_content', 'table_html', 'count_title', 'count_html'])->first();

            echo getmypid() . ' ' . date('Y-m-d H:i:s') . ' id:' . $keyword['id'] . ' project_id:' . $task->project_id . PHP_EOL;

            $update = false;
            if (empty($keyword['sale_title'])) {
                $sale_title = $this->new_title($keyword['title'], $prefix, $suffix);
                $keyword->sale_title = $sale_title;
                $update = true;
            }
            if (empty($keyword['sale_content']) && $keyword->sale_title) {
                $content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_sale_content']['ai']);
                if ($content) {
                    $keyword->sale_content = $content;
                    $update = true;
                }
            }
            if (empty($keyword['table_html']) && $keyword->sale_title) {
                $content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_data_table']['ai']);
                if ($content) {
                    $keyword->table_html = str_replace(['```html', '``html', '```'], '', $content);
                    $update = true;
                }
            }
            if (empty($keyword['count_title'])) {
                $count_title = $this->new_title($keyword['title'], $prefix, $suffix);
                $count_title && $keyword->count_title = $count_title;
                $update = true;
            }
            if (empty($keyword['count_html']) && $keyword->sale_title) {
                $content = $this->ai_send($keyword->sale_title, $company_en_description, $ai_commands['tag_count_content']['ai']);
                if ($content) {
                    $keyword->count_html = $this->fixChart(str_replace(['```html', '``html', '```'], '', $content));
                    $update = true;
                }
            }
            if ($update) {
                $keyword->save();
                $update_rows++;
            }
        }
        return $update_rows;
    }

    public function new_title($title, $prefix, $suffix)
    {
        //打乱顺序
        shuffle($prefix);
        shuffle($suffix);
        //标题(title):{聚合页扩展标题前缀} keywords {聚合页扩展标题后缀} {聚合页扩展标题后缀}

        return sprintf('%s %s %s %s', $prefix[0], $title, $suffix[0], $suffix[1]);
    }


    public function ai_send($title, $company_description, $prompt)
    {
        if (strpos($prompt, '{title}') !== false) {
            $prompt = str_replace('{title}', $title, $prompt);
        }
        if (strpos($prompt, '{company introduction}') !== false) {
            $prompt = str_replace('{company introduction}', $company_description, $prompt);
        }
        if (strpos($prompt, '{chart_type}') !== false) {
            shuffle($this->chart_types);
            dump($this->chart_types[0]);
            $prompt = str_replace('{chart_type}', $this->chart_types[0], $prompt);
        }

        $text = Gpt::instance()->openai_chat_qqs($prompt);
        if (!$text) {
            echo getmypid() . ' ' . '生成失败' . PHP_EOL;
        }
        return $text;
    }

    function fixChart($html)
    {
        $html = '<body>' . $html . '</body>';
        $dom = new \DOMDocument();
        @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
        $canvas_count = $dom->getElementsByTagName('canvas')->count();
        //没有canvas
        if (!$canvas_count) {
            $div = $dom->getElementsByTagName('div');
            foreach ($div as $element) {
                if ($element->hasAttribute('id')) {
                    $canvas = $dom->createElement('canvas');
                    $canvas->setAttribute('id', $element->getAttribute('id'));
                    $element->removeAttribute('id');
                    $element->appendChild($canvas);
                    break;
                }
            }
        }
        $body = $dom->getElementsByTagName('body')->item(0);
        $modifiedHtml = '';
        foreach ($body->childNodes as $child) {
            $modifiedHtml .= $dom->saveHTML($child);
        }
        return $modifiedHtml;
    }

    public function sendNotify($project_id)
    {
        //获取当前项目的域名
        $domainModel = new DomainInfo();
        $domainInfo = $domainModel->read(['project_id' => $project_id]);
        if ($domainInfo === false) {
            //获取测试域名
            $deployBuildModel = new DeployBuild();
            $buildInfo = $deployBuildModel->read(['project_id' => $project_id]);
            $domain = $buildInfo['test_domain'];
        } else {
            $domain = 'https://' . $domainInfo['domain'] . '/';
        }
        $url = $domain . 'api/update_page/';
        $param = [
            'project_id' => $project_id,
            'type' => 1,
            'route' => 2,
            'url' => [],
            'language' => [],
        ];
        NoticeLog::createLog(NoticeLog::GENERATE_PAGE, json_encode(['c_url' => $url, 'c_params' => $param]), date('Y-m-d H:i:s', time() + 300));
        echo getmypid() . ' ' . '更新中请稍后, 更新完成将会发送站内信通知更新结果!' . PHP_EOL;
    }
}