我是基于Kimi moonshot-v1-8k实现的AI助手,在此博客上负责整理和概括文章

本文介绍了如何在shokaX主题的博客中搭建和更换图床。首先,作者分享了使用CloudFlare R2和Telegram Bot搭建免费图床的方法,但指出速度一般。随后,为了提升图片加载速度,作者转向国内平台,使用阿里云OSS搭建图床,并指出了搭建过程中的一个常见错误。接着,文章详细描述了如何处理图片并自动上传到图床,包括将图片转换为WebP格式、重命名、存储和上传的过程。此外,作者还讨论了如何在shokaX博客中更换图床,包括修改配置文件和代码,以适应新的图床服务。最后,提到了如何通过修改代码和配置,实现分类卡片封面的远程封面链接配置,以及如何更新图片JSON文件格式。文章还简要提到了更换Butterfly博客图床的方法,即通过替换旧图床链接为新图床链接的方式。

编辑记录

2025-02-20 第一次编辑

- 正文


# CloudFlare 图床搭建

主要参考文档进行部署和搭建。当前已经有 v2.0 版本,但我使用的仍是 v1.0 版本,结合 Telegram BotCloudflare R2 搭建图床。该图床的特点是:免费、容量足够、速度还行。但是如果对图片加载速度有追求,则不建议。

处理并上传图片的代码:

upload.py
import os
import shutil
from PIL import Image
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
# 配置代理绕过规则
os.environ["NO_PROXY"] = "cloudflare-imgbed-9os.pages.dev"
class CustomHttpAdapter(HTTPAdapter):
    """自定义适配器,用于解决 HTTPS 主机名检查问题"""
    def __init__(self, *args, **kwargs):
        self._ssl_context = create_urllib3_context()
        super().__init__(*args, **kwargs)
    def init_poolmanager(self, *args, **kwargs):
        kwargs['ssl_context'] = self._ssl_context
        return super(CustomHttpAdapter, self).init_poolmanager(*args, **kwargs)
def convert_to_webp(input_dirs, output_dir, folder_labels, quality=90):
    """
    将多个文件夹中的图片转换为 WebP 格式,并按文件夹字符串重命名。
    Args:
        input_dirs (list): 输入文件夹的路径列表。
        output_dir (str): 转换后的图片存储的总文件夹路径。
        folder_labels (dict): 每个文件夹对应的字符串标签,格式为 {文件夹路径: 标签字符串}。
        quality (int): WebP 图片质量。
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    print(f"开始转换图片,输出目录: {output_dir}")
    for input_dir in input_dirs:
        if input_dir not in folder_labels:
            print(f"跳过未配置标签的文件夹: {input_dir}")
            continue
        label = folder_labels[input_dir]
        count = 1
        for root, dirs, files in os.walk(input_dir):
            # 忽略子文件夹
            if root != input_dir:
                continue
            for file in files:
                input_path = os.path.join(root, file)
                output_filename = f"{label}_{count}.webp"
                output_path = os.path.join(output_dir, output_filename)
                try:
                    with Image.open(input_path) as img:
                        img.convert("RGB").save(output_path, "webp", quality=quality)
                        print(f"转换成功: {input_path} -> {output_path}")
                        count += 1
                except Exception as e:
                    print(f"转换失败: {input_path}, 错误: {e}")
def upload_images(api_url, output_dir, params=None, user_agent=None):
    """
    上传图片到 CloudFlare 图床。
    Args:
        api_url (str): 上传接口 URL。
        output_dir (str): 包含图片的文件夹路径。
        params (dict): 可选的查询参数。
        user_agent (str): 自定义 User-Agent。
    """
    session = requests.Session()
    session.mount("https://", CustomHttpAdapter())
    headers = {"User-Agent": user_agent or "Mozilla/5.0"}
    for file in os.listdir(output_dir):
        file_path = os.path.join(output_dir, file)
        try:
            with open(file_path, 'rb') as image_file:
                files = {'file': image_file}
                response = session.post(api_url, params=params, files=files, headers=headers)
                if response.status_code == 200:
                    print(f"上传成功: {file}")
                else:
                    print(f"上传失败: {file}, 状态码: {response.status_code}, 错误信息: {response.text}")
        except Exception as e:
            print(f"上传失败: {file}, 错误: {e}")
if __name__ == "__main__":
    # 输入文件夹列表
    input_dirs = [
        "C:\\Users\\10116\\Pictures\\game\\summer pockets",
        "C:\\Users\\10116\\Pictures\\EVA\\webp\\background",
        # "C:\\Users\\10116\\Pictures\\EVA\\webp\\cover",
        "C:\\Users\\10116\\Pictures\\KON\\webp",
        "C:\\Users\\10116\\Pictures\\Ghibli",
    ]
    # 每个文件夹对应的标签
    folder_labels = {
        "C:\\Users\\10116\\Pictures\\game\\summer pockets": "sp",
        "C:\\Users\\10116\\Pictures\\EVA\\webp\\background": "eva",
        # "C:\\Users\\10116\\Pictures\\EVA\\webp\\cover": "eva",
        "C:\\Users\\10116\\Pictures\\KON\\webp": "kon",
        "C:\\Users\\10116\\Pictures\\Ghibli": "ghibli",
    }
    # 输出总文件夹
    output_dir = "C:/Users/10116/Pictures/BlogImages"
    
    # API 上传配置
    api_url = "https://cloudflare-imgbed-9os.pages.dev/upload"
    params = {
        "authCode": "4869",  # 替换为您的认证码
        "uploadNameType": "origin",
        "returnFormat": "full"
    }
    user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    # 转换图片
    convert_to_webp(input_dirs, output_dir, folder_labels, quality=90)
    # 上传图片
    upload_images(api_url, output_dir, params, user_agent)

该代码功能是把目标文件夹的图片转换为 webp 并按标签重命名后保存到某个总文件夹后,将该文件夹的图片依次上传到图床。

# 阿里云 OSS 图床搭建

为了优化图片加载速度,选择用国内厂商的平台搭建图床,阿里云 OSS 新用户注册免费送三个月 20G 存储容量,且性价比高,因此选择作为图床平台。搭建流程参考文档,按步骤一步步操作最终可实现通过 PicGo 上传图片到图床,但注意该文档有一处错误:

这句话:划重点!这里一定要注意,设定存储区域里要填的是之前记下来的地域节点里面的第一个字段,比如你的地域节点值是 "oss-cn-shanghai.aliyuncs.com",那么这里只需要填"oss-cn-shanghai",切记,否则配置失败无法上传图片。
实际请在” 概览 “里的” 访问端口 “->” 外网访问 “->"Endpoint(地域节点)" 查看,例如为 oss-cn-wuhan-lr.aliyuncs.com ,则这里要填 oss-cn-wuhan-lr 而不是 "oss-cn-wuhan"!

# 处理图片并自动上传

实现:

  • 遍历多个输入文件夹,将其中的图片转换为 WebP 格式(转换时会将图片转为 RGB 模式),
  • 按照每个文件夹对应的标签和序号重命名(如 eva_1.webpkon_2.webp 等),
  • 将转换后的图片存储到统一的输出文件夹中,
  • 最后将输出文件夹中所有 WebP 文件的路径(绝对路径)构造成一个 JSON 对象,
  • 并通过 HTTP POST 请求上传到指定的接口(请求体格式为 JSON ,示例为 {"list": ["xxx.webp", ...] })。
upload.py
import os
from PIL import Image
import requests
def convert_to_webp(input_dirs, output_dir, folder_labels, quality=90):
    """
    将多个文件夹中的图片转换为 WebP 格式,并按文件夹标签重命名后存储到总文件夹中。
    Args:
        input_dirs (list): 输入图片文件夹的路径列表。
        output_dir (str): 输出图片存储的总文件夹路径。
        folder_labels (dict): 每个输入文件夹对应的标签,例如 {文件夹路径: "标签字符串"}。
        quality (int): 输出 WebP 图片的质量(0-100)。
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    print(f"开始转换图片,输出目录: {output_dir}")
    for input_dir in input_dirs:
        if input_dir not in folder_labels:
            print(f"跳过未配置标签的文件夹: {input_dir}")
            continue
        label = folder_labels[input_dir]
        count = 1
        # 仅处理当前文件夹内的文件,不递归子文件夹
        for root, dirs, files in os.walk(input_dir):
            if root != input_dir:
                continue
            for file in files:
                input_path = os.path.join(root, file)
                output_filename = f"{label}_{count}.webp"
                output_path = os.path.join(output_dir, output_filename)
                try:
                    with Image.open(input_path) as img:
                        # 转换为 RGB 后保存为 WebP 格式
                        img.convert("RGB").save(output_path, "webp", quality=quality)
                    print(f"转换成功: {input_path} -> {output_path}")
                    count += 1
                except Exception as e:
                    print(f"转换失败: {input_path},错误: {e}")
def upload_images(api_url, output_dir):
    """
    将输出文件夹中的所有图片路径以 JSON 格式上传到指定接口。
    上传请求的要求:
      - HTTP 方法:POST
      - URL:如 http://127.0.0.1:36677/upload(示例默认配置)
      - 请求体:{"list": ["完整路径1", "完整路径2", ...]},必须为 JSON 格式
    Args:
        api_url (str): 上传接口的 URL。
        output_dir (str): 存储转换后图片的文件夹路径。
    """
    # 获取输出目录下所有 WebP 文件(完整路径)
    file_list = [
        os.path.join(output_dir, f)
        for f in os.listdir(output_dir)
        if f.lower().endswith(".webp")
    ]
    if not file_list:
        print("没有找到需要上传的图片。")
        return
    payload = {"list": file_list}
    headers = {"Content-Type": "application/json"}
    try:
        response = requests.post(api_url, json=payload, headers=headers)
        if response.status_code == 200:
            print("图片上传成功!")
        else:
            print(f"上传失败,状态码: {response.status_code},响应: {response.text}")
    except Exception as e:
        print(f"上传过程中发生错误: {e}")
if __name__ == "__main__":
    # 定义输入图片文件夹列表
    input_dirs = [
        r"C:\Users\10116\Pictures\test",
    ]
    # 定义每个文件夹对应的标签
    folder_labels = {
        r"C:\Users\10116\Pictures\test": "test",
    }
    # 输出图片存储的总文件夹
    output_dir = "C:\\Users\\10116\\Pictures\\tt"
    # 上传接口 URL(默认配置示例)
    api_url = "http://127.0.0.1:36677/upload"
    
    # 先转换图片
    convert_to_webp(input_dirs, output_dir, folder_labels, quality=90)
    
    # 再上传图片
    upload_images(api_url, output_dir)

# 更换 shokaX 博客图床

选择在 _config.shokaX.yml 配置一个参数: imgbed 方便后续管理和切换。新增参数:

# imgbed: "https://cloudflare-imgbed-9os.pages.dev/file/"
imgbed: "https://miyano.oss-cn-wuhan-lr.aliyuncs.com/img/"

于是需要修改下面的文件进行适配。

# 修改 themes\shokaX\layout\gallery.pug

方法:获取主题配置文件里的参数 imgbed ,如果 gallery.yml 里的 img 为链接形式则不动,否则进行拼接为 url。

extends ./_partials/layout.pug
block content
  - var imgbed = theme.imgbed || '';
  div#gallery-container
    div.gallery-group-main
      each group in page.posts 
        a(href=group.url target="_blank") 
          figure.gallery-group
            - var imgSrc = /^https?:\/\//.test(group.img) ? group.img : imgbed + group.img;
            img.gallery-group-img(src=imgSrc alt=group.name)
            figcaption
              div.gallery-group-name= group.name
              p= group.descr
  include _partials/pagination.pug
extends ./_partials/layout.pug
block content
  div#gallery-container
    //- h1.page-title 相册
    div.gallery-group-main
      each group in page.posts 
        a(href=group.url target="_blank") 
          figure.gallery-group
            img.gallery-group-img(src=group.img alt=group.name)
            figcaption
              div.gallery-group-name= group.name
              p= group.descr
  include _partials/pagination.pug

# 修改 themes\shokaX\layout\gallery-item.pug

方法:同样获取参数 imgbed ,如果 gallery.yml 里的 photos 里的每个 photo 为链接形式则不动,否则进行拼接为 url。

extends ./_partials/layout.pug
block content
  - var imgbed = theme.imgbed || '';
  div.gallery-container.gallery-items
    each photo in page.photos
      - var photoSrc = /^https?:\/\//.test(photo) ? photo : imgbed + photo;
      a(href=photoSrc class="gallery-item" data-src=photoSrc)
        img(src=photoSrc alt=page.group.name title=page.group.descr loading="lazy")
extends ./_partials/layout.pug
block content
  div.gallery-container.gallery-items
    each photo in page.photos
      a(href=photo class="gallery-item" data-src=photo)
        img(src=photo alt=page.group.name title=page.group.descr loading="lazy")

# 修改 source\_data\gallery.yml

只需要将原来的:

gallery_groups:
  - name: KON
    descr: KON一生推,毕业不是终点,今后也都是朋友。
    url: /miyano/gallery/kon/
    img: "https://cloudflare-imgbed-9os.pages.dev/file/kon_16.webp"
    photos:
      - "https://cloudflare-imgbed-9os.pages.dev/file/kon_16.webp"

改为:

gallery_groups:
  - name: KON
    descr: KON一生推,毕业不是终点,今后也都是朋友。
    url: /miyano/gallery/kon/
    img: "kon_16.webp"
    photos:
      - "kon_16.webp"

# 修改分类卡片的封面逻辑

原先的效果是,必须在分类文件夹里放一张名字为 cover.jpg (或其他格式) 的图片才能渲染出分类卡片,但是这样的实际效果是渲染的速度对于质量高的图片不佳,于是决定进行修改,使卡片的封面可以通过读取图片 url 得到。

于是通过全局搜索 cover.jpg 再一步步串联找到需要修改的文件: themes\shokaX\layout\_mixin\card.pugthemes\shokaX\scripts\generaters\index.js

具体改进方法:在 _config.shokaX.yml 新设置参数: cover_map ,格式为:

cover_map:
  Literature-Translation: "kon_16.webp"
  Playful-Exploration:  "kon_3.webp"
  Scientific-Notes: "kon_8.webp"
  Engine-Translation: "kon_11.webp"

即为每个分类配置一个短图片链接。

对于 card.pug ,修改如下:

mixin CardRender(item)
    - var timestamp = Date.now()
    - var cover = item.cover ? item.cover : (url_for(theme.statics + item.slug + '/cover.jpg') + '?v=' + timestamp)
    - var itemname = item.name
mixin CardRender(item)
    - var timestamp = Date.now() 
    - cover = url_for(theme.statics + item.slug + '/cover.jpg') + '?v=' + timestamp
    //- - cover = url_for(theme.statics + item.slug + '/cover.jpg')
    - itemname = item.name

方法是:修改 mixin 部分,先检测传入的分类项是否有 cover 属性(即通过 cover_map 配置得到的远程封面链接),如果存在则直接使用,否则继续使用原有通过静态资源目录生成的本地封面链接。

对于 index.js ,修改如下:

// 从主题配置中获取远程封面相关参数
  const themeConfig = hexo.theme.config;
  const coverMap = themeConfig.cover_map || {};
  const imgbed = themeConfig.imgbed || "";
  if (categories && categories.length) {
    categories.forEach((cat) => {
      // 优先判断 cover_map 配置
      if (coverMap[cat.slug]) {
        // 使用远程封面链接
        cat.cover = imgbed + coverMap[cat.slug];
        // 处理分类的归属关系及子分类 / 文章数据(与本地封面处理逻辑一致)
        const topcat = getTopcat(cat);
        if (topcat._id !== cat._id) {
          cat.top = topcat;
        }
        const child = categories.find({ parent: cat._id });
        let pl = 6;
        if (child.length !== 0) {
          cat.child = child.length;
          cat.subs = child.sort({ name: 1 }).limit(6).toArray();
          pl = Math.max(0, pl - child.length);
          if (pl > 0) {
            cat.subs.push(...cat.posts.sort({ title: 1 }).filter(function(item, i) {
              return item.categories.last()._id === cat._id;
            }).limit(pl).toArray());
          }
        } else {
          cat.subs = cat.posts.sort({ title: 1 }).limit(6).toArray();
        }
        catlist.push(cat);
      } else {
        // 没有配置 cover_map,则使用本地封面图片
        const localCoverDir = `source/_posts/${cat.slug}`;
        if (fs.existsSync(localCoverDir + "/cover.avif")) {
          covers.push({
            path: cat.slug + "/cover.avif",
            data: function() {
              return fs.createReadStream(localCoverDir + "/cover.avif");
            }
          });
        } else if (fs.existsSync(localCoverDir + "/cover.webp")) {
          covers.push({
            path: cat.slug + "/cover.webp",
            data: function() {
              return fs.createReadStream(localCoverDir + "/cover.webp");
            }
          });
        } else if (fs.existsSync(localCoverDir + "/cover.jpg")) {
          covers.push({
            path: cat.slug + "/cover.jpg",
            data: function() {
              return fs.createReadStream(localCoverDir + "/cover.jpg");
            }
          });
          const topcat = getTopcat(cat);
          if (topcat._id !== cat._id) {
            cat.top = topcat;
          }
          const child = categories.find({ parent: cat._id });
          let pl = 6;
          if (child.length !== 0) {
            cat.child = child.length;
            cat.subs = child.sort({ name: 1 }).limit(6).toArray();
            pl = Math.max(0, pl - child.length);
            if (pl > 0) {
              cat.subs.push(...cat.posts.sort({ title: 1 }).filter(function(item, i) {
                return item.categories.last()._id === cat._id;
              }).limit(pl).toArray());
            }
          } else {
            cat.subs = cat.posts.sort({ title: 1 }).limit(6).toArray();
          }
          catlist.push(cat);
        }
      }
    });
  }
if (categories && categories.length) {
    categories.forEach((cat) => {
      const cover = `source/_posts/${cat.slug}`;
      if (fs.existsSync(cover + "/cover.avif")) {
        covers.push({
          path: cat.slug + "/cover.avif",
          data: function() {
            return fs.createReadStream(cover + "/cover.avif");
          }
        });
      } else if (fs.existsSync(cover + "/cover.webp")) {
        covers.push({
          path: cat.slug + "/cover.webp",
          data: function() {
            return fs.createReadStream(cover + "/cover.webp");
          }
        });
      } else if (fs.existsSync(cover + "/cover.jpg")) {
        covers.push({
          path: cat.slug + "/cover.jpg",
          data: function() {
            return fs.createReadStream(cover + "/cover.jpg");
          }
        });
        const topcat = getTopcat(cat);
        if (topcat._id !== cat._id) {
          cat.top = topcat;
        }
        const child = categories.find({ parent: cat._id });
        let pl = 6;
        if (child.length !== 0) {
          cat.child = child.length;
          cat.subs = child.sort({ name: 1 }).limit(6).toArray();
          pl = Math.max(0, pl - child.length);
          if (pl > 0) {
            cat.subs.push(...cat.posts.sort({ title: 1 }).filter(function(item, i) {
              return item.categories.last()._id === cat._id;
            }).limit(pl).toArray());
          }
        } else {
          cat.subs = cat.posts.sort({ title: 1 }).limit(6).toArray();
        }
        catlist.push(cat);
      }
    });
  }

方法是:在生成器中,从主题配置中读取 imgbedcover_map 参数。遍历每个分类时,若 cover_map 中存在该分类的配置,则为该分类添加 cover 属性,并构建出完整的远程封面链接;否则则采用原有判断本地 cover 图片的逻辑。注意:当使用远程封面时,不再从本地读取文件,而是直接为分类对象设置封面链接,同时其它分类数据(如 topsubs 等)保持原有处理逻辑,并将该分类加入 catlist

通过上述修改,成功实现通过读取 cover_map 进行分类卡片的封面配置。

# 修改 updateYaml.js

需要更新该代码的逻辑,实现:

  1. 首页与文章图片生成
    脚本先从 images.json 中读取当前主题(由 MyTheme 或默认随机选择)的 homearticles 数组,将 imgbed 参数拼接后写入对应的 YAML 文件。
  2. 远程封面分配( cover_map 功能)
    _config.shokaX.yml 中存在 cover_map 配置时:
  • images.json 中读取当前主题的 cover 数组。
  • 随机选取与 cover_map 键数量相同的封面(确保不重复),并更新 cover_map 的值。
  • 将更新后的配置写回 _config.shokaX.yml (以便后续生成器中使用远程封面链接)。
  • 同时,对那些分类文件夹名称未包含在 cover_map 键中的,仍使用原来的本地封面复制逻辑生成 cover.jpg
  1. 原有功能保留
    当配置文件中没有 cover_map 时,所有博客分类文件夹均通过随机选取本地主题封面图片生成 cover.jpg 文件。

改进代码如下:

const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
// JSON 文件路径
const jsonFilePath = path.join(__dirname, "themes/shokaX/images.json");
// YAML 文件路径(用于生成首页与文章图片列表)
const indexYmlPath = path.join(__dirname, "themes/shokaX/_images_index.yml");
const articleYmlPath = path.join(__dirname, "themes/shokaX/_images.yml");
// 主题图片文件夹路径(本地封面备用)
const coverImgDir = path.join(__dirname, "source/_data/coverimg");
// 博客文章文件夹路径
const postsDir = path.join(__dirname, "source/_posts");
// 主题配置文件路径
const configFilePath = path.join(__dirname, "_config.shokaX.yml");
/**
 * 组合 imgbed URL 与 imageName,若 imageName 已为完整 URL 则直接返回
 */
const getFullImageUrl = (imgbed, imageName) => {
  return /^https?:\/\//.test(imageName) ? imageName : `${imgbed}${imageName}`;
};
/**
 * 随机从数组中选取 n 个元素(不保证原数组顺序)
 */
const getRandomElements = (array, n) => {
  const shuffled = array.sort(() => 0.5 - Math.random());
  return shuffled.slice(0, n);
};
const updateYmlFiles = async () => {
  // 读取 _config.shokaX.yml 配置
  let config;
  try {
    config = yaml.load(fs.readFileSync(configFilePath, "utf8"));
  } catch (error) {
    console.error("Error reading _config.shokaX.yml:", error);
    return;
  }
  // 获取 imgbed 参数
  const imgbed = config.imgbed || "";
  console.log("imgbed:", imgbed);
  // 获取 MyTheme 参数(若为 default 则根据日期选取一个主题)
  let theme = config.MyTheme || "default";
  if (theme === "default") {
    const themes = ["kon", "sp", "eva", "ghibli"];
    const currentDate = new Date();
    // 转换为北京时间
    const beijingTime = new Date(currentDate.getTime() + 8 * 60 * 60 * 1000);
    const dayOfYear = (date) => {
      const start = new Date(date.getFullYear(), 0, 0);
      const diff = date - start + (start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000;
      const oneDay = 1000 * 60 * 60 * 24;
      return Math.floor(diff / oneDay);
    };
    const dayIndex = dayOfYear(beijingTime);
    const themeIndex = dayIndex % themes.length;
    theme = themes[themeIndex];
  }
  console.log("Selected Theme:", theme);
  // 读取 images.json 数据
  const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, "utf8"));
  // 生成首页 (home) 与文章 (articles) 图片列表(拼接 imgbed URL)
  const homeImages = (jsonData[theme]?.home || []).map((img) => getFullImageUrl(imgbed, img));
  const articleImages = (jsonData[theme]?.articles || []).map((img) => getFullImageUrl(imgbed, img));
  // 写入 YAML 文件
  fs.writeFileSync(indexYmlPath, homeImages.map((url) => `- ${url}`).join("\n"), "utf8");
  fs.writeFileSync(articleYmlPath, articleImages.map((url) => `- ${url}`).join("\n"), "utf8");
  console.log(`YAML files updated for theme: ${theme}`);
  // 获取本地封面图片(备用方案)所在目录
  const themeCoverDir = path.join(coverImgDir, theme);
  let themeImages = [];
  if (fs.existsSync(themeCoverDir)) {
    themeImages = fs.readdirSync(themeCoverDir).filter((file) => /\.(jpg|jpeg|png|webp)$/i.test(file));
  } else {
    console.error(`Theme folder not found: ${themeCoverDir}`);
  }
  if (themeImages.length === 0) {
    console.error(`No images found in theme folder: ${themeCoverDir}`);
  }
  // 获取博客文章文件夹(每个分类文件夹)
  const postFolders = fs.readdirSync(postsDir).filter((item) => {
    const itemPath = path.join(postsDir, item);
    return fs.statSync(itemPath).isDirectory();
  });
  /**
   * 当 _config.shokaX.yml 中配置了 cover_map 时,
   * 则从 images.json 对应主题下的 cover 数组中随机选取且不重复的图片,
   * 用于更新 cover_map 中各分类的封面值;
   * 同时,对于 cover_map 中已定义的分类,不再生成本地 cover.jpg,
   * 其他分类仍保持原来的随机复制方式。
   */
  if (config.cover_map && Object.keys(config.cover_map).length > 0) {
    const coverMapKeys = Object.keys(config.cover_map);
    // 读取 images.json 中远程封面图片数组
    const coverImagesRemote = jsonData[theme]?.cover || [];
    if (coverImagesRemote.length < coverMapKeys.length) {
      console.error(`Not enough remote cover images available for cover_map assignment for theme: ${theme}`);
    } else {
      // 随机选取不重复的图片赋值给 cover_map 中的每个分类(键)
      const shuffled = coverImagesRemote.sort(() => 0.5 - Math.random());
      coverMapKeys.forEach((key, index) => {
        config.cover_map[key] = shuffled[index];
      });
      // 将更新后的 cover_map 写回 _config.shokaX.yml 文件
      fs.writeFileSync(configFilePath, yaml.dump(config), "utf8");
      console.log("Updated cover_map in _config.shokaX.yml:", config.cover_map);
    }
    // 对于本地封面复制,只处理不在 cover_map 中的分类文件夹
    const localFolders = postFolders.filter(folder => !coverMapKeys.includes(folder));
    if (localFolders.length > 0 && themeImages.length > 0) {
      const selectedImages = getRandomElements(themeImages, localFolders.length);
      localFolders.forEach((folder, index) => {
        const postFolderPath = path.join(postsDir, folder);
        const sourceImagePath = path.join(themeCoverDir, selectedImages[index]);
        const outputImagePath = path.join(postFolderPath, "cover.jpg");
        fs.copyFileSync(sourceImagePath, outputImagePath);
        console.log(`cover.jpg created for ${folder}`);
      });
    }
  } else {
    // 若未配置 cover_map,则使用原来的方式,为所有分类文件夹随机复制本地封面图片
    if (themeImages.length > 0) {
      const selectedImages = getRandomElements(themeImages, postFolders.length);
      postFolders.forEach((folder, index) => {
        const postFolderPath = path.join(postsDir, folder);
        const sourceImagePath = path.join(themeCoverDir, selectedImages[index]);
        const outputImagePath = path.join(postFolderPath, "cover.jpg");
        fs.copyFileSync(sourceImagePath, outputImagePath);
        console.log(`cover.jpg created for ${folder}`);
      });
    }
  }
};
updateYmlFiles();
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
// JSON 文件路径
const jsonFilePath = path.join(__dirname, "themes/shokaX/images.json");
// YAML 文件路径
const indexYmlPath = path.join(__dirname, "themes/shokaX/_images_index.yml");
const articleYmlPath = path.join(__dirname, "themes/shokaX/_images.yml");
// 主题图片文件夹路径
const coverImgDir = path.join(__dirname, "source/_data/coverimg");
// 博客文章文件夹路径
const postsDir = path.join(__dirname, "source/_posts");
// 主题配置文件路径
const configFilePath = path.join(__dirname, "_config.shokaX.yml");
// 读取 `_config.shokaX.yml` 并获取 `imgbed` 参数
const getImgBedUrl = () => {
  try {
    const config = yaml.load(fs.readFileSync(configFilePath, "utf8"));
    return config.imgbed || ""; // 如果 `imgbed` 没有配置,返回空字符串
  } catch (error) {
    console.error("Error reading _config.shokaX.yml:", error);
    return "";
  }
};
// 读取 `_config.shokaX.yml` 并获取 `MyTheme` 参数
const getTheme = () => {
  try {
    const config = yaml.load(fs.readFileSync(configFilePath, "utf8"));
    return config.MyTheme || "default"; // 如果 `imgbed` 没有配置,返回空字符串
  } catch (error) {
    console.error("Error reading theme config:", error);
    return "default";
  }
};
//// 获取当前主题逻辑
// const getTheme = () => {
//   try {
//     const config = fs.readFileSync(configFilePath, "utf8");
//     const match = config.match(/MyTheme:\s*(\S+)/);
//     return match ? match[1] : "default";
//   } catch (error) {
//     console.error("Error reading theme config:", error);
//     return "default";
//   }
// };
// 组合 `imgbed` URL 和 `imageName`
const getFullImageUrl = (imgbed, imageName) => {
  return /^https?:\/\//.test(imageName) ? imageName : `${imgbed}${imageName}`;
};
// 随机从数组中选取 n 个元素
const getRandomElements = (array, n) => {
  const shuffled = array.sort(() => 0.5 - Math.random());
  return shuffled.slice(0, n);
};
// 更新 YAML 文件
const updateYmlFiles = async () => {
  const imgbed = getImgBedUrl(); // 获取 `imgbed` 配置
  console.log("imgbed:", imgbed);
  const theme = getTheme(); // 获取当前主题
  const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, "utf8"));
  let selectedTheme = theme;
  if (theme === "default") {
    const themes = ["kon", "sp", "eva", "ghibli"];
    const currentDate = new Date();
    const beijingTime = new Date(currentDate.getTime() + 8 * 60 * 60 * 1000); // 转换为北京时间
    
    // 根据日期计算主题索引
    const dayOfYear = (date) => {
      const start = new Date(date.getFullYear(), 0, 0);
      const diff = date - start + (start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000;
      const oneDay = 1000 * 60 * 60 * 24;
      return Math.floor(diff / oneDay);
    };
    const dayIndex = dayOfYear(beijingTime);
    const themeIndex = dayIndex % themes.length;
    selectedTheme = themes[themeIndex];
  }
  console.log("Selected Theme:", selectedTheme);
  // 获取 `home` 和 `articles` 图片,并拼接 `imgbed`
  const homeImages = (jsonData[selectedTheme]?.home || []).map((img) => getFullImageUrl(imgbed, img));
  const articleImages = (jsonData[selectedTheme]?.articles || []).map((img) => getFullImageUrl(imgbed, img));
  // 写入 YAML 文件
  fs.writeFileSync(indexYmlPath, homeImages.map((url) => `- ${url}`).join("\n"), "utf8");
  fs.writeFileSync(articleYmlPath, articleImages.map((url) => `- ${url}`).join("\n"), "utf8");
  console.log(`YAML files updated for theme: ${selectedTheme}`);
  // 获取主题对应的图片文件夹
  const themeCoverDir = path.join(coverImgDir, selectedTheme);
  if (!fs.existsSync(themeCoverDir)) {
    console.error(`Theme folder not found: ${themeCoverDir}`);
    return;
  }
  // 获取主题图片列表
  const themeImages = fs.readdirSync(themeCoverDir).filter((file) => /\.(jpg|jpeg|png|webp)$/i.test(file));
  if (themeImages.length === 0) {
    console.error(`No images found in theme folder: ${themeCoverDir}`);
    return;
  }
  // 获取博客文章文件夹数量
  const postFolders = fs.readdirSync(postsDir).filter((item) => {
    const itemPath = path.join(postsDir, item);
    return fs.statSync(itemPath).isDirectory();
  });
  // 随机选取图片并生成 cover.jpg
  const selectedImages = getRandomElements(themeImages, postFolders.length);
  postFolders.forEach((folder, index) => {
    const postFolderPath = path.join(postsDir, folder);
    const sourceImagePath = path.join(themeCoverDir, selectedImages[index]);
    const outputImagePath = path.join(postFolderPath, "cover.jpg");
    // 复制图片为 cover.jpg
    fs.copyFileSync(sourceImagePath, outputImagePath);
    console.log(`cover.jpg created for ${folder}`);
  });
};
updateYmlFiles();

同时, themes\shokaX\images.json 的格式需改为类似:

{
    "kon": {
      "home": [
        "kon_1.webp" 
      ],
      "articles": [
        "kon_16.webp"
      ],
      "cover":[
        "kon_18.webp"
      ]
    },
}
{
    "kon": {
      "home": [
        "https://cloudflare-imgbed-9os.pages.dev/file/kon_1.webp"
      ],
      "articles": [
        "https://cloudflare-imgbed-9os.pages.dev/file/kon_16.webp"
      ]
    },
}

# 修改 themes\shokaX\scripts\helpers\engine.js

当修改了 source\_data\gallery.yml 里的 photos 里的图片为短链接时,例如 "kon_16.webp",会导致进入相册页面的图库时,页面的头图显示出错,此时的链接为: src="/miyano/kon_16.webp" ,导致出错。所以应该修改这里的判断逻辑,一步步顺藤摸瓜,"themes\shokaX\layout\_partials\layout.pug"->"themes\shokaX\scripts\helpers\engine.js",于是修改该文件:

hexo.extend.helper.register("_image_url", function(img, path = "") {
  const { statics, imgbed } = hexo.theme.config;
  const { post_asset_folder } = hexo.config;
  if (img.startsWith("//") || img.startsWith("http")) {
    return img;
  } else if (imgbed && imgbed.trim() !== "") {
    // 如果配置了 imgbed,则使用它拼接完整的 URL
    const base = imgbed.endsWith('/') ? imgbed : imgbed + '/';
    return base + img;
  } else {
    return import_hexo_util.url_for.call(this, statics + (post_asset_folder ? path : "") + img);
  }
});
hexo.extend.helper.register("_image_url", function(img, path = "") {
  const { statics } = hexo.theme.config;
  const { post_asset_folder } = hexo.config;
  if (img.startsWith("//") || img.startsWith("http")) {
    return img;
  } else {
    return import_hexo_util.url_for.call(this, statics + (post_asset_folder ? path : "") + img);
  }
});

实现:当 imgbed 配置存在并且不是空时,直接使用 imgbed + img 来拼接 URL。如果 imgbed 配置为空,则继续使用原来的 statics 路径。从而避免因为 statics 拼接而产生的错误。此外,为确保 URL 拼接正确,还需要确认 imgbed 最后是否有斜杠,因此可以使用类似 imgbed.endsWith ('/') ? imgbed : imgbed + '/' 来保证这一点。

# 修改 themes\shokaX\layout\_partials\sidebar\overview.pug

为了使博客的头像也能使用 url 获取得到,而不是直接使用本地的图片,于是修改 overview.pug ,当判断到 theme.imgbed 存在且不为空时,会直接使用 imgbed + theme.sidebar.avatar 生成完整的 URL,否则仍使用原来的 url_for 拼接生成路径。

div(class="author" itemprop="author" itemscope itemtype="http://schema.org/Person")
    - var avatarUrl = "";
    if theme.imgbed && theme.imgbed.trim() !== ""
        - avatarUrl = theme.imgbed + theme.sidebar.avatar;
    else
        - avatarUrl = url_for(theme.statics + theme.assets + '/' + theme.sidebar.avatar);
    img(loading="lazy" decoding="async" class="image" itemprop="image" alt=author src=avatarUrl)
div(class="author" itemprop="author" itemscope itemtype="http://schema.org/Person")
    img(loading="lazy" decoding="async" class="image" itemprop="image" alt=author
        src=url_for(theme.statics + theme.assets + '/'+ theme.sidebar.avatar))

_config.shokaX.yml 中配置为:

sidebar:
  # Sidebar Position.
  position: left
  # position: right
  # Replace the default avatar image under <root>/source/_data/assets/ and set the url here.
  avatar: ai-icon.webp

实际上没有变化,只是由 themes\shokaX\source\assets\ai-icon.webp 变成了 imgbed+ai-icon.webp

# 更换 Butterfly 博客图床

对于旧的博客,代码结构化不好,于是采用了暴力替换图床字符串链接的方式,为每个旧图床链接进行了新图床的链接替换。