← 返回列表 media.php 22.8 KB · 2026-11-06 10:19
<?php
// ===== 源码查看保护 =====
// 本文件同时用于服务器运行(cron)和在线查看源码。
// 通过 Web 直接访问时,仅以纯文本输出自身源码,不执行任何业务逻辑。
// 注意:gk_auto.php 若被 Web 执行会真的触发发文,这层保护是必需的。
if (PHP_SAPI !== 'cli' && basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
    header('Content-Type: text/plain; charset=utf-8');
    echo file_get_contents(__FILE__);
    exit;
}
/**
 * media.php — 文章配图模块(百度图片方案 + 多维质量打分)
 *
 * 为什么需要打分:百度图片结果质量参差(水印/字幕/教科书翻拍/厂商广告),
 * 盲取第一张必然出问题。百度返回了 clarityScore、原始尺寸、来源页标题、
 * 来源站点等信号,据此评分择优。
 *
 * 过滤链:
 *   1. 关键词质量   —— 只提核心名词,剔除"我们/为什么/但你会看吗"这类碎片
 *   2. 结果预筛     —— clarityScore、尺寸、长宽比、来源站点黑名单、来源标题相关性
 *   3. 图像校验     —— 魔数 + 可解码 + 分辨率 + 色彩丰富度(识别翻拍/截图)
 *   4. 综合打分     —— 多个维度加权,取最高分
 */

/** 图片尺寸下限 */
const GK_IMG_MIN_W = 700;
const GK_IMG_MIN_H = 380;
/** 目标最大宽度 */
const GK_IMG_MAX_W = 1200;
/** JPEG 质量 */
const GK_IMG_QUALITY = 82;
/** 最低接受分数 */
const GK_IMG_MIN_SCORE = 40;

/** 低质来源站点(论坛/问答/素材站/游戏站,多为截图水印图) */
const GK_BAD_HOSTS = [
    'zhidao.baidu.com', 'wenku.baidu.com', 'baike.baidu.com', 'jingyan.baidu.com',
    'tieba.baidu.com', 'bbs.', 'forum.', 'download.', 'zol.com.cn', 'pcauto.com.cn',
    'taobao.com', 'tmall.com', 'jd.com', '1688.com', 'made-in-china.com',
    'docin.com', 'doc88.com', 'book118.com', 'yxlady.com', 'duitang.com',
    'bilibili.com', 'douyin.com', 'kuaishou.com', '3dmgame.com', 'ali213.net',
];

/**
 * 提取核心关键词(规则版,作为 AI 失败时的兜底)
 * 目标:拿到能搜到"主题照片"的名词,而不是句子碎片
 */
function gk_keywords_rule($title, $content = '') {
    $pool = [];
    if ($content !== '' && preg_match_all('#<h2[^>]*>(.*?)</h2>#is', $content, $hm)) {
        foreach ($hm[1] as $h) $pool[] = trim(strip_tags($h));
    }
    $t = preg_replace('/[((].*?[))]/u', ' ', $title);
    $parts = preg_split('/[,,。.、;;::||—]/u', $t);
    if ($parts) $pool[] = trim($parts[0]);

    $stop = ['我们','你们','他们','这个','那个','这些','那些','一个','一种','可以','已经','还是','不是',
             '没有','就是','因为','所以','但是','而且','如果','虽然','这样','那样','自己','时候','东西',
             '事情','问题','地方','真的','其实','开始','需要','不会','能','会','要','就','都','也','还',
             '被','把','让','给','对','从','到','在','和','与','或','的','了','着','过','吗','呢','吧','啊',
             '为什么','怎么样','如何','怎么','什么','是不是','能不能','会不会','该不该','要不要','有没有',
             '别急','别再','千万','一定','到底','究竟','明明','原来','凭什','谁说','你会','你不','教你',
             '浅谈','浅析','关于','那些','这些'];

    $words = [];
    foreach ($pool as $seg) {
        // 只取完整语义块(2-5 字),不做人工截断
        if (preg_match_all('/[\x{4e00}-\x{9fa5}]{2,5}/u', $seg, $m)) {
            foreach ($m[0] as $w) {
                if (in_array($w, $stop, true)) continue;
                if (preg_match('/^(的|了|着|过|与|和|在|从)/u', $w)) continue;
                if (preg_match('/(的|了|着|过)$/u', $w)) continue;
                if (preg_match('/^[0-9一二三四五六七八九十百千万]+(年|月|日|天|分|秒|小时)?$/u', $w)) continue;
                $words[] = $w;
            }
        }
    }
    $words = array_values(array_unique($words));
    usort($words, function ($a, $b) {
        return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8');
    });
    return array_slice($words, 0, 2);
}

/**
 * 用 AI 生成精准搜图关键词
 * 规则切词会切出「与低压」「种典型」这类无意义碎片,
 * 交给模型判断语义最可靠。
 *
 * @return array ['cn'=>['中文词'], 'en'=>['英文词']] 或 null
 */
function gk_keywords_ai($title, $content, $cfg) {
    // 本模块可能被独立脚本引用(如一次性发布脚本),此时主程序的
    // ai_chat / extract_json 不存在,直接退回规则关键词,不要崩。
    if (!function_exists('ai_chat') || !function_exists('extract_json')) {
        return null;
    }
    $plain = trim(mb_substr(strip_tags($content), 0, 700));
    $prompt = "文章标题:{$title}\n\n文章开头:{$plain}\n\n"
        . "请为这篇文章选配一张**真实摄影照片**,需要给出图片搜索关键词。\n"
        . "要求:\n"
        . "1. 关键词必须是**具体可拍摄的实物或场景**,2-4 个汉字,不要抽象概念。\n"
        . "   好例:电动汽车、充电桩、单反相机、城市夜景、电影院\n"
        . "   坏例:产业发展、时间结构、现代改写、与低压、种典型\n"
        . "2. 给 3 组中文关键词,从最贴切到最宽泛;再给对应英文关键词 2-3 个。\n"
        . "3. 如果是影评,用影片类型对应的场景(如战争片→战场、科幻片→太空站),不要用片名(片名会搜到无关东西)。\n"
        . "只输出 JSON:{\"cn\":[\"词1\",\"词2\",\"词3\"],\"en\":[\"word1\",\"word2\"]}";

    $raw = ai_chat($prompt, '你是图片编辑,负责为文章配图挑搜索关键词。只输出 JSON。', $cfg);
    if (strpos($raw, 'AI_CALL_FAILED::') === 0) return null;

    $j = extract_json($raw);
    if (!is_array($j)) return null;

    $cn = array_values(array_filter(array_map(function ($w) {
        return trim((string)$w);
    }, (array)($j['cn'] ?? []))));
    $en = array_values(array_filter(array_map(function ($w) {
        return trim((string)$w);
    }, (array)($j['en'] ?? []))));

    if (!$cn && !$en) return null;
    return ['cn' => array_slice($cn, 0, 3), 'en' => array_slice($en, 0, 3)];
}

/** 统一入口:优先 AI,失败退回规则 */
function gk_keywords($title, $content = '', $cfg = null) {
    if (is_array($cfg)) {
        $ai = gk_keywords_ai($title, $content, $cfg);
        if ($ai && !empty($ai['cn'])) {
            return $ai['cn'];
        }
    }
    return gk_keywords_rule($title, $content);
}

/**
 * 中文 → 英文关键词(内置词表,供参考显示;百度走中文)
 */
function gk_translate_kw($cnWords) {
    $map = [
        '摄影'=>'photography','相机'=>'camera','镜头'=>'lens','光圈'=>'aperture','虚化'=>'bokeh',
        '曝光'=>'exposure','色彩'=>'color','人像'=>'portrait','风景'=>'landscape','夜景'=>'night',
        '航拍'=>'drone','视频'=>'video','电池'=>'battery','充电'=>'charging','电动'=>'electric',
        '汽车'=>'car','手机'=>'smartphone','芯片'=>'chip','人工智能'=>'ai','算法'=>'algorithm',
        '电影'=>'film','影评'=>'cinema','导演'=>'director','纪录片'=>'documentary','动画'=>'animation',
        '太空'=>'space','航天'=>'aerospace','科学'=>'science','历史'=>'history','文学'=>'literature',
        '旅行'=>'travel','美食'=>'food','健康'=>'health','建筑'=>'architecture','城市'=>'city',
    ];
    $out = [];
    foreach ($cnWords as $w) {
        foreach ($map as $k => $v) {
            if (mb_strpos($w, $k, 0, 'UTF-8') !== false) { $out[] = $v; break; }
        }
    }
    return array_slice(array_values(array_unique($out)), 0, 3);
}

/* =========================================================
 * HTTP
 * =======================================================*/
function gk_http_get($url, $timeout = 15, $maxBytes = 6 * 1024 * 1024) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_CONNECTTIMEOUT => 8,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_ENCODING       => '',
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => 0,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120',
        CURLOPT_HTTPHEADER     => ['Accept: image/*,*/*;q=0.8', 'Referer: https://image.baidu.com/'],
        CURLOPT_NOPROGRESS     => false,
        CURLOPT_PROGRESSFUNCTION => function ($r, $dl) use ($maxBytes) {
            return ($dl > $maxBytes) ? 1 : 0;
        },
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($code !== 200 || !is_string($body) || $body === '') return '';
    return $body;
}

/* =========================================================
 * 图源:百度图片(含预筛)
 * =======================================================*/
function gk_search_baidu($kwWords, $want = 20) {
    if (!$kwWords) return [];
    $word = urlencode(implode(' ', $kwWords));
    $url = "https://image.baidu.com/search/acjson?tn=resultjson_com&logid=1&ipn=rj"
         . "&ct=201326592&fp=result&word={$word}&pn=0&rn={$want}&gsm=1e";

    $raw = gk_http_get($url, 15);
    if ($raw === '') return [];

    $j = json_decode($raw, true);
    if (!is_array($j) || empty($j['data'])) {
        $j = json_decode(str_replace('\\/', '/', $raw), true);
    }
    if (!is_array($j) || empty($j['data'])) return [];

    $cands = [];
    foreach ($j['data'] as $item) {
        if (!is_array($item)) continue;
        $u = $item['hoverURL'] ?? ($item['middleURL'] ?? ($item['thumbURL'] ?? ''));
        if (!is_string($u) || $u === '') continue;
        $u = preg_replace('#^http://#i', 'https://', $u);

        $host  = strtolower((string)($item['fromURLHost'] ?? ''));
        $srcTitle = (string)($item['fromPageTitleEnc'] ?? '');
        $cw = (int)($item['width'] ?? 0);
        $chh = (int)($item['height'] ?? 0);
        $clarity = (int)($item['clarityScore'] ?? 0);

        // 黑名单站点直接丢
        $bad = false;
        foreach (GK_BAD_HOSTS as $b) {
            if ($host !== '' && strpos($host, $b) !== false) { $bad = true; break; }
        }
        if ($bad) continue;

        // 原始尺寸太小 / 长宽比异常 直接丢(放宽:百度 width/height 是原图尺寸,
        // 部分结果缺失或不准确,过严会把可用图全部筛掉)
        if ($cw > 0 && $chh > 0) {
            if ($cw < 640 || $chh < 360) continue;
            $r = $cw / $chh;
            if ($r < 0.85 || $r > 3.4) continue;      // 允许略竖的图,但仍排除长篇截图
        }

        // 清晰度太低的丢(放宽到 35)
        if ($clarity > 0 && $clarity < 35) continue;

        // 来源标题里出现明显不相关的词(游戏/攻略/下载/小说)直接丢
        if (preg_match('/(游戏|攻略|下载|小说|漫画|壁纸|头像|表情包|视频教程|破解|注册码)/u', $srcTitle)) {
            continue;
        }

        // 旅游/消费/促销类来源降权标记(这类图多半是打卡照、广告图,与严肃内容不搭)
        $softPenalty = 0;
        if (preg_match('/(体验|打卡|探店|游艇|度假|旅游攻略|价格|优惠|促销|优惠券|多少钱|测评|开箱)/u', $srcTitle)) {
            $softPenalty = 18;
        }
        // 软件界面/截图类降权(不是摄影照片,配文章显得廉价)
        if (preg_match('/(软件|界面|截图|客户端|app|工具下载|安装包|系统设置|后台|教程步骤)/iu', $srcTitle)) {
            $softPenalty += 30;
        }

        $cands[] = [
            'url'     => $u,
            'host'    => $host,
            'srcText' => $srcTitle,
            'w'       => $cw,
            'h'       => $chh,
            'clarity' => $clarity,
            'penalty' => $softPenalty,
        ];
    }
    return $cands;
}

/**
 * 图像质量评估(识别翻拍纸张 / 截图 / 纯色图)
 * @return array ['score'=>0-100, 'reasons'=>[]]
 */
function gk_image_quality($im) {
    $w = imagesx($im); $h = imagesy($im);
    $score = 50;
    $reasons = [];

    // 采样网格
    $grid = 40;
    $satSum = 0; $lumSum = 0; $n = 0;
    $lumMin = 255; $lumMax = 0;
    $edgeWhite = 0;   // 边缘近白像素(翻拍纸张特征)

    for ($gy = 0; $gy < $grid; $gy++) {
        for ($gx = 0; $gx < $grid; $gx++) {
            $px = (int)($gx * $w / $grid);
            $py = (int)($gy * $h / $grid);
            if ($px >= $w) $px = $w - 1;
            if ($py >= $h) $py = $h - 1;
            $c = imagecolorat($im, $px, $py);
            $r = ($c >> 16) & 0xFF; $g = ($c >> 8) & 0xFF; $b = $c & 0xFF;

            $mx = max($r, $g, $b); $mn = min($r, $g, $b);
            $sat = $mx == 0 ? 0 : ($mx - $mn) / $mx;        // 0..1
            $lum = 0.299*$r + 0.587*$g + 0.114*$b;          // 0..255

            $satSum += $sat; $lumSum += $lum; $n++;
            if ($lum < $lumMin) $lumMin = $lum;
            if ($lum > $lumMax) $lumMax = $lum;

            // 边缘区域白像素统计
            if ($gx < 4 || $gx >= $grid - 4 || $gy < 4 || $gy >= $grid - 4) {
                if ($lum > 215 && $sat < 0.12) $edgeWhite++;
            }
        }
    }
    $avgSat = $n ? $satSum / $n : 0;
    $avgLum = $n ? $lumSum / $n : 0;
    $range  = $lumMax - $lumMin;

    // ① 色彩丰富度:照片通常有明显饱和;翻拍文档/截图偏灰白
    if ($avgSat >= 0.28)      { $score += 22; $reasons[] = '色彩丰富'; }
    elseif ($avgSat >= 0.18)  { $score += 12; }
    elseif ($avgSat < 0.08)   { $score -= 25; $reasons[] = '几乎无色彩(疑文档/截图)'; }
    else                      { $score -= 6; }

    // ② 亮度分布:过曝/过暗都差
    if ($avgLum < 35)         { $score -= 20; $reasons[] = '过暗'; }
    elseif ($avgLum > 225)    { $score -= 22; $reasons[] = '过亮(疑白底文档)'; }
    elseif ($avgLum >= 60 && $avgLum <= 185) { $score += 10; }

    // ③ 动态范围:照片有层次,纯色/低对比图差
    if ($range < 60)          { $score -= 18; $reasons[] = '对比度极低(疑纯色/截图)'; }
    elseif ($range > 150)     { $score += 8; }

    // ④ 边缘大面积为白 → 典型"翻拍纸张/文档截图"
    $edgeRatio = $n ? $edgeWhite / ($n * 0.36) : 0;   // 边缘约占 36%
    if ($edgeRatio > 0.55)    { $score -= 30; $reasons[] = '边缘大面积白色(疑翻拍文档)'; }
    elseif ($edgeRatio > 0.35) { $score -= 12; }

    // ⑤ 分辨率加分
    if ($w >= 1000 && $h >= 500) { $score += 6; }

    // ⑥ 角落水印粗检:水印多为某角落一小块高对比、高亮度的区域,
    //    与周边形成突变。检查四角 18% 区域内是否存在这种"异常块"。
    $warnCorner = false;
    $cw = (int)($w * 0.18); $ch = (int)($h * 0.18);
    $corners = [[0,0], [$w-$cw,0], [0,$h-$ch], [$w-$cw,$h-$ch]];
    foreach ($corners as $ci => $corner) {
        $bright = 0; $tot = 0; $contrast = 0;
        for ($y = $corner[1]; $y < $corner[1] + $ch; $y += 3) {
            for ($x = $corner[0]; $x < $corner[0] + $cw; $x += 3) {
                if ($x < 0 || $y < 0 || $x >= $w || $y >= $h) continue;
                $c = imagecolorat($im, $x, $y);
                $r = ($c >> 16) & 0xFF; $g = ($c >> 8) & 0xFF; $b = $c & 0xFF;
                $lum = 0.299*$r + 0.587*$g + 0.114*$b;
                if ($lum > 235) $bright++;
                $tot++;
            }
        }
        // 角落出现大量近白像素(白字水印常见形态)
        if ($tot > 50 && ($bright / $tot) > 0.10) { $warnCorner = true; break; }
    }
    if ($warnCorner) { $score -= 10; $reasons[] = '角落疑有水印'; }

    return ['score' => max(0, min(100, $score)), 'reasons' => $reasons,
            'sat' => round($avgSat, 2), 'lum' => round($avgLum), 'range' => $range];
}

/**
 * 下载 + 校验 + 规整一张图
 * @return array|null ['data','w','h','ext','quality']
 */
function gk_load_image($url) {
    $bin = gk_http_get($url, 20);
    if ($bin === '' || strlen($bin) < 4000) return null;

    $isJpg  = (substr($bin, 0, 2) === "\xFF\xD8");
    $isPng  = (substr($bin, 0, 4) === "\x89PNG");
    $isWebp = (substr($bin, 0, 4) === 'RIFF' && substr($bin, 8, 4) === 'WEBP');
    if (!$isJpg && !$isPng && !$isWebp) return null;

    $im = @imagecreatefromstring($bin);
    if (!$im) return null;

    $w = imagesx($im); $h = imagesy($im);
    if ($w < GK_IMG_MIN_W || $h < GK_IMG_MIN_H) { imagedestroy($im); return null; }

    $q = gk_image_quality($im);

    // 缩放
    if ($w > GK_IMG_MAX_W) {
        $nw = GK_IMG_MAX_W;
        $nh = (int)round($h * ($nw / $w));
        $dst = imagecreatetruecolor($nw, $nh);
        imagecopyresampled($dst, $im, 0, 0, 0, 0, $nw, $nh, $w, $h);
        imagedestroy($im);
        $im = $dst; $w = $nw; $h = $nh;
    }

    ob_start();
    imagejpeg($im, null, GK_IMG_QUALITY);
    $out = ob_get_clean();
    imagedestroy($im);
    if (!is_string($out) || strlen($out) < 4000) return null;

    return ['data' => $out, 'w' => $w, 'h' => $h, 'ext' => 'jpg', 'quality' => $q];
}

/**
 * 按主题取一张配图(主入口)
 * @param array $cfg 传入配置时启用 AI 关键词;不传则用规则兜底
 * @return array|null ['data','w','h','ext','src','url','score','quality']
 */
function gk_fetch_topic_image($title, $content = '', $cfg = null, $log = null) {
    // 关键词:AI 优先,规则兜底;同时拿到多组候选词轮试
    $kwSets = [];
    if (is_array($cfg)) {
        $ai = gk_keywords_ai($title, $content, $cfg);
        if ($ai && !empty($ai['cn'])) {
            foreach ($ai['cn'] as $w) { $kwSets[] = [$w]; }
            // 两两组合也试一次(更具体)
            if (count($ai['cn']) >= 2) { $kwSets[] = [$ai['cn'][0], $ai['cn'][1]]; }
            if ($log) $log("    AI关键词: " . implode(' / ', $ai['cn']));
        }
    }
    if (!$kwSets) {
        $rule = gk_keywords_rule($title, $content);
        if ($rule) {
            $kwSets[] = $rule;
            foreach ($rule as $w) { $kwSets[] = [$w]; }
            if ($log) $log("    规则关键词: " . implode(' / ', $rule));
        }
    }
    if (!$kwSets) return null;

    $best = null;

    foreach ($kwSets as $kw) {
        $cands = gk_search_baidu($kw, 20);
        if ($log) $log("    搜「" . implode(' ', $kw) . "」→ " . count($cands) . " 张候选");
        if (!$cands) continue;

        usort($cands, function ($a, $b) { return $b['clarity'] <=> $a['clarity']; });

        $tried = 0;
        foreach ($cands as $c) {
            if ($tried >= 6) break;
            $tried++;
            $img = gk_load_image($c['url']);
            if (!$img) continue;

            $total = $img['quality']['score'];
            $total += (int)round($c['clarity'] * 0.25);
            if ($c['w'] >= 1600) $total += 6;
            $total -= (int)($c['penalty'] ?? 0);          // 旅游/消费类降权

            if ($log) {
                $log(sprintf("      试%d %dx%d 清晰%d 质量%d 合计%d %s",
                    $tried, $img['w'], $img['h'], $c['clarity'],
                    $img['quality']['score'], $total,
                    $img['quality']['reasons'] ? '(' . implode('/', $img['quality']['reasons']) . ')' : ''));
            }

            if (!$best || $total > $best['score']) {
                $best = $img;
                $best['score'] = $total;
                $best['src'] = 'Baidu';
                $best['url'] = $c['url'];
                $best['srcText'] = $c['srcText'];
                $best['kw'] = $kw;
            }
            if ($total >= 88) break 2;   // 足够好,整体停止
        }
    }

    if (!$best) return null;
    if ($best['score'] < GK_IMG_MIN_SCORE) {
        if ($log) $log("    ✗ 最高分 {$best['score']} 低于阈值 " . GK_IMG_MIN_SCORE . ",放弃配图");
        return null;
    }
    return $best;
}

/**
 * 把图片写入 WP 媒体库
 */
function gk_save_media($conn, $prefix, $siteDir, $siteUrl, $img, $title) {
    $ym = date('Y/m');
    $uploadDir = rtrim($siteDir, '/') . '/wp-content/uploads/' . $ym;
    if (!is_dir($uploadDir)) @mkdir($uploadDir, 0755, true);
    if (!is_dir($uploadDir) || !is_writable($uploadDir)) return null;

    $base = preg_replace('/[^\p{Han}a-zA-Z0-9]+/u', '-', mb_substr($title, 0, 30));
    $base = trim($base, '-');
    if ($base === '') $base = 'post';
    $name = $base . '-' . substr(md5(uniqid('', true)), 0, 6) . '.jpg';
    $path = $uploadDir . '/' . $name;

    if (file_put_contents($path, $img['data']) === false) return null;
    @chmod($path, 0644);
    @chown($path, 'www');
    @chgrp($path, 'www');

    $relFile = $ym . '/' . $name;
    $url     = rtrim($siteUrl, '/') . '/wp-content/uploads/' . $relFile;
    $now     = date('Y-m-d H:i:s');
    $nowG    = gmdate('Y-m-d H:i:s');
    $titleEsc = $conn->real_escape_string($title);
    $guid     = $conn->real_escape_string($url);

    $sql = "INSERT INTO {$prefix}posts
            (post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt,
             post_status, comment_status, ping_status, post_password, post_name,
             to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered,
             post_parent, guid, menu_order, post_type, post_mime_type, comment_count)
            VALUES (1, '{$now}', '{$nowG}', '', '{$titleEsc}', '',
                    'inherit', 'open', 'closed', '', '',
                    '', '', '{$now}', '{$nowG}', '', 0, '{$guid}', 0,
                    'attachment', 'image/jpeg', 0)";
    if (!$conn->query($sql)) { @unlink($path); return null; }
    $aid = $conn->insert_id;

    $relEsc = $conn->real_escape_string($relFile);
    $meta = serialize([
        'width'  => $img['w'], 'height' => $img['h'],
        'file'   => $relFile, 'sizes'  => [],
        'image_meta' => [
            'aperture' => '0', 'credit' => '', 'camera' => '', 'caption' => '',
            'created_timestamp' => '0', 'copyright' => '', 'focal_length' => '0',
            'iso' => '0', 'shutter_speed' => '0', 'title' => '', 'orientation' => '0',
        ],
    ]);
    $metaEsc = $conn->real_escape_string($meta);

    $conn->query("INSERT INTO {$prefix}postmeta (post_id, meta_key, meta_value) VALUES
                  ({$aid}, '_wp_attached_file', '{$relEsc}'),
                  ({$aid}, '_wp_attachment_metadata', '{$metaEsc}'),
                  ({$aid}, '_wp_attachment_image_alt', '{$titleEsc}')");

    return ['url' => $url, 'path' => $path, 'attach_id' => $aid, 'rel' => $relFile];
}

/** 正文顶部配图 HTML(居中、圆角、与截图样式一致) */
function gk_image_html($url, $alt) {
    $altEsc = htmlspecialchars($alt, ENT_QUOTES, 'UTF-8');
    $urlEsc = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
    return '<figure class="wp-block-image size-large gk-hero-img">'
         . '<img src="' . $urlEsc . '" alt="' . $altEsc . '" '
         . 'style="width:100%;max-width:900px;height:auto;display:block;margin:0 auto 30px;border-radius:10px;" />'
         . '</figure>';
}