# 创建备份仓库

这里使用 node 作为上传服务

在目录创建upload.js文件,假设已经存在一个publicServe的方法上传文章

获取修改文件

通过git diff可以判断哪些文章有修改。要注意如果文章名包含中文,需要设置git config --global core.quotepath false,防止出现中文连码的情况。

javascript
import { execSync } from "child_process";

/**
 * 获取两个 Git 提交之间的文件差异
 * @param {string} latestCommitSha - 最新的 Git 提交 SHA
 * @param {string} prevCommitSha - 上一个 Git 提交 SHA
 * @return {{ oldFilePath: string; newFilePath: string }[]} - 返回一个对象,包含 status, oldFilePath, newFilePath
 */
function getDiffFiles(latestCommitSha, prevCommitSha) {
  let diffOutput;
  if (!prevCommitSha || !latestCommitSha) {
    diffOutput = execSync(`git diff --name-status HEAD^ HEAD`).toString();
  } else {
    diffOutput = execSync(`git diff --name-status ${prevCommitSha} ${latestCommitSha}`).toString();
  }
  const diffArray = diffOutput.split("\n");
  // 根据文件状态将文件分组
  return diffArray
    .map((line) => {
      const [_, status, oldFilePath, newFilePath] = line.match(/(?:^([AMDR])\d*\s+([^\s]*)\s*([^\s]*)$)/) || [];
      return status && oldFilePath ? { status, oldFilePath, newFilePath: newFilePath || oldFilePath || "" } : null;
    })
    .filter((item) => item && item.newFilePath.toLocaleLowerCase().endsWith(".md"))
    .reduce((acc, { oldFilePath, newFilePath }) => {
      acc.push({ oldFilePath, newFilePath });
      return acc;
    }, []);
}

这里并没有返回文件状态(修改,新增等),因为目前没有不考虑删除的情况,考虑到上传失败的情况,这些状态并没有太多的参考价值。

生成文章开篇

这里使用 chatgpt 生成开篇,使用chatgpt包,chatgpt 的 apiKey 通常保存在github secrets中,通过环境变量或者参数传递进来。考虑到 gpt 服务可能不稳定,一般需要多试几遍。

javascript
import { ChatGPTAPI } from "chatgpt";

const apiKey = process.env.API_KEY;
async function generateArticleIntro(content) {
  for (let i = 1; i < 3; i++) {
    try {
      const api = new ChatGPTAPI({
        apiKey,
      });
      const res = await api.sendMessage(`给下面的文章添加一个简要的开篇\n ${content}`);
      if (res.text) {
        return res.text;
      }
    } catch (e) {
      if (i < 3) console.log("重试第" + i + "次");
      else throw e;
    }
  }
  return "";
}

获取之前执行失败的文件

生成开篇或者上传都可能存在失败的情况,需要我们记录下来,读取并添加到本次的流程中。

javascript
/**
 * 获取修改文件并与上传失败记录合并
 * @param {string} latestCommitSha - 最新的 Git 提交 SHA
 * @param {string} prevCommitSha - 上一个 Git 提交 SHA
 * @param {{ [oldFilePath: string]: { oldFilePath: string; newFilePath: string } }} preFailRecord - 上传失败记录
 * @return {{ oldFilePath: string; newFilePath: string }[]} - 返回一个对象,包含 status, oldFilePath, newFilePath
 */
function ensureDiffFiles(latestCommitSha, prevCommitSha, preFailRecord) {
  const diffFiles = getDiffFiles(latestCommitSha, prevCommitSha);
  for (const [failFilename, file] of Object.entries(preFailRecord)) {
    if (diffFiles.some((item) => item.newFilePath === file.newFilePath && item.oldFilePath === file.oldFilePath)) {
      continue;
      // 当前旧文件名和记录旧文件名相同,说明文件二次重名名,删除旧的失败记录
    } else if (diffFiles.some((item) => item.oldFilePath === file.oldFilePath)) {
      delete preFailRecord[failFilename];
      continue;
      // 当前新文件名和记录旧文件名相同,说明文件在失败基础上修改,旧文件名改为失败记录的
    } else if (diffFiles.some((item) => item.oldFilePath === file.newFilePath)) {
      const index = diffFiles.findIndex((item) => item.oldFilePath === file.newFilePath);
      diffFiles[index].oldFilePath = file.oldFilePath;
      delete preFailRecord[failFilename];
      continue;
    } else {
      diffFiles.push(file);
    }
  }
  return diffFiles;
}

preFailRecord 是记录上次执行失败的文件。这里需要考虑文件重命名的情况,删除等更复杂的情况暂时没有处理。

保存记录

在这里有三类数据需要额外记录

  1. 上次触发 action 的 sha,因为操作提交多次然后才上传的情况,不可能直接取上一次提交 sha。

  2. 失败记录,就是前面 preFailRecord 参数,因为不保证上传能一次性成功,需要加入后续 action 中重新执行

  3. 文件对应的上传数据,例如上传完成后返回的文件 id,用于更新或者判断是否需要生成开篇。

后面的问题是在哪里保存这些数据

  1. 在本仓库下保存,好处是容易读取和修改,缺点是提交记录会很难看

  2. 通过 action 缓存记录,缺点不易于本地测试,读取和修改也比较麻烦

  3. 通过服务器缓存,缺点是比较繁琐,需要额外的维护

  4. 子模块,这个无疑是最合适的方案,随时修改和读取,也可以免费托管到 github 上,也不会影响主仓库 git 提交记录

git 子模块可以参考这个文档

首先在 github 创建一个缓存仓库,这里起名 cache

然后添加到仓库下

shell
git submodule add https://github.com/user/cache

会看到一个 cache 目录,然后在主模块目录下推送上去就好了。

这里需要在子模块(cache 目录下)创建 3 个文件, sha.json   recordFail.json record.json ,对应上面三类数据,初始值写入{}就可以。

然后在 cache 目录下执行git push就可以更新子模块了

整合

根据上传结果更新 json 文件就可以

javascript
import { readFileSync, writeFileSync } from "fs";

async function main() {
  const prevCommitSha = readJsonFile("./cache/sha.json")?.sha;
  const latestCommitSha = process.env.GITHUB_SHA;
  const fileRecord = readJsonFile("./cache/record.json");
  const preFailRecord = readJsonFile("./cache/recordFail.json");
  const diffFiles = ensureDiffFiles(latestCommitSha, prevCommitSha, preFailRecord);
  for (const file of diffFiles) {
    const { oldFilePath, newFilePath } = file;
    const toSaveDate = {
      title: getFileName(newFilePath),
      content: readFileSync(newFilePath, "utf-8"),
    };
    // 是否未记录,未记录则生成开篇
    try {
      if (!fileRecord[oldFilePath]) {
        const overview = await generateArticleIntro(toSaveDate.content);
        toSaveDate.overview = overview;
      } else {
        toSaveDate.id = fileRecord[oldFilePath];
      }
      const id = await publicServe(toSaveDate);
      if (id) {
        // 处理重命名
        if (oldFilePath !== newFilePath) delete fileRecord[oldFilePath];
        fileRecord[newFilePath] = id;
        delete preFailRecord[newFilePath];
      } else {
        preFailRecord[newFilePath] = file;
      }
    } catch (e) {
      preFailRecord[newFilePath] = file;
      console.log(e);
    }
  }
  saveJsonFile("./cache/sha.json", { sha: latestCommitSha });
  saveJsonFile("./cache/record.json", fileRecord);
  saveJsonFile("./cache/recordFail.json", preFailRecord);
}

function getFileName(path) {
  return path.split("/").pop().split(".")[0];
}
// 读取 json 文件
function readJsonFile(path) {
  return JSON.parse(readFileSync(path, "utf-8"));
}
// 保存 json 文件
function saveJsonFile(path, data) {
  writeFileSync(path, JSON.stringify(data, null, 2));
}

增加 action

创建文件.github/workflows/upload.yml

yml
name: Upload

on:
  push:
    branches: [main]

permissions: write-all

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: checkout
        uses: actions/checkout@v3
        with:
          submodules: recursive
          token: ${{ secrets.TOKEN }}
          fetch-depth: 0

      - uses: pnpm/action-setup@v2
        with:
          version: 6.0.2

      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "pnpm"

      - name: update submodule
        run: git submodule update --recursive --remote

      - name: run upload
        run: |
          git config --global core.quotepath false
          pnpm install
          node upload.js
        env:
          API_KEY: ${{ secrets.API_KEY }}

      - name: commit changes
        run: |
          git config --global user.name "yjrhgvbn"
          git config --global user.email "yjrhgvbn@gmail.com"
          cd cache
          git commit -a -m "Add record"

      - name: Push changes
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.TOKEN }}
          directory: "./cache"
          repository: "yjrhgvbn/cache"
          force: true

action 这里不多结束,只有有几个注意的地方

  1. permissions: write-all需要增加写入权限,不然无法更新文件

  2. fetch-depth: 0,默认情况下actions/checkout@v3只会拉取最后一次提交,这里需要拉取之前的提交进行 diff

  3. git submodule update --recursive --remote,拉取最新的子模块

  4. git config --global core.quotepath false,防止 diff 时中文乱码

最后

完整项目可以查看项目