使用 Github Action 自动同步 obisidian 和 hexo 仓库,避免手动操作。
1. 烦恼
先来说说慕雪现在的笔记和博客是怎么管理的吧,我正在使用两套笔记软件
- 思源笔记:私密性高一些,不是博客的笔记都在这里面。由于思源笔记不是 markdown 编辑器,不能直接和 hexo 对接;
- obisdian:专门管理 hexo 的博客;
然后我的 hexo 博客和 obsidian 又有分离,hexo 配置仓库是一个单独的 git 仓库(后文简称为 hexo 仓库),obsidian 博客库也是一个单独的 git 仓库(后文简称为 obisidian 仓库)。
我采用的操作特别繁琐,步骤如下:
- 在 obsidian 里面写好博客之后,手动使用 FreeFileSync 软件,将
obisidian/blog
目录同步到 hexo/source/_posts
目录中(这两个目录完全一样); - 然后再到 hexo 本地仓库中执行 hexo 三板斧命令,给新的博客生成 abbrlink,push 到 hexo 的 github 仓库;
- 再用 FreeFileSync 反向将
hexo/source/_posts
目录同步回 obisidian/blog
目录,因为新的博客会多出 abbrlink;
是不是听起来都头大了?
2. 曾经的想法
先前我一直在想怎么让这套流程简化,考虑过几个方案都不太满意。我想过直接把 obsidian vaults 丢到 hexo/source/_posts
目录里面,但是考虑到我的 obsidian 中还有博客模板这种不需要上传到博客里面的内容,此项并不方便(虽然 hexo 其实可以跳过渲染某些 md 文件)
现在就想出了自动化的方案,也就是用 github action 来同步 obsidian 和 hexo 的仓库,当 obisidian/blog
目录有变动的时候,触发 action,自动将这个目录的内容拷贝到 hexo/source/_posts
仓库目录中,并 push 到 hexo 仓库。
这里就有一个问题,abbrlink 是基于 hexo 插件生成的,如果用这种方式那就没办法给新的博客 md 文件生成一个固定 abbrlink 了。不管是怎么让 github action 执行 hexo g
命令,最后都会出现远程仓库 md 文件中有 abbrlink,但本地需要 pull 才能更新的问题,这会对我后续的博客编写和 git 操作带来不便(毕竟之前都是无脑 push 上去的)
之前每次想折腾 github action 的时候就会发现这个问题(由于没记笔记导致折腾的时候忘记了之前为啥没搞定……),然后又不了了之。
今天突然想起来,既然问题是在 abbrlink 插件上,那我不用 hexo 来生成 abbrlink 不就行了?反正 abbrlink 本质上和随机数没啥关系,我只要给新的博客手动加上一个和其他博客不冲突的 abbrlink 不就 ok 了?
注:hexo 的 abbrlink 插件是通过 crc16/crc32 算法计算得到文件的 abbrlink 的,并非随机数生成。但对于 abbrlink 的作用来看,只要博客上每个文章都有一个独立的 abbrlink 其实就够了,所以 abbrlink 说它是随机数也没啥问题。
解决方法明了:用别的方法给新博客生成 abbrlink,然后再用 github action 自动化同步 obsidian 仓库和 hexo 仓库。
3. 解决步骤
3.1. 生成 abbrlink 的 python 脚本
其实 obsidian 中是有一个 abbrlink 插件的,首先感谢插件作者能提供一个 hexo-abbrlink 插件的替代品。但是,这个插件不太符合本人的需求,因为它直接针对于 obsidian 全局,会把我的其他文件以及博客模板文件都加上 abbrlink。
折腾了一会后,感觉不如返璞归真,直接写个 python 脚本,把所有博客文件的 abbrlink 遍历出来,然后生成 30 个不冲突的 abbrlink 写入到一个文件里面,每次写新博客的时候从这个文件里面取一个 abbrlink 出来用就完事啦!
说干就干,GPT,启动!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| import yaml import re import os import random
MD_FILE_PATH = '../../Notes/CODE' """博客md文件路径""" NEW_ABBRLINK_SIZE = 20 """生成几个abbrlink""" NEW_ABBRLINK_MD_FILE = '../../Notes/ABBRLINK归档.md' """生成的abbrlink写入这个md文件里面"""
def extract_front_matter(file_path): """ 提取 Markdown 文件中的 front-matter 内容。 假设 front-matter 是以 '---' 包围的 YAML 格式内容。 """ with open(file_path, 'r', encoding='utf-8') as file: content = file.read() match = re.match(r'---\n(.*?)\n---\n', content, re.DOTALL) if match: front_matter = match.group(1) return yaml.safe_load(front_matter) else: return None
def remove_front_matter(file_path): """ 移除 Markdown 文件中的 front-matter 部分,返回去除 front-matter 后的内容。 """ with open(file_path, 'r', encoding='utf-8') as file: content = file.read() cleaned_content = re.sub(r'---\n(.*?)\n---\n', '', content, flags=re.DOTALL) return cleaned_content
def update_front_matter(file_path, new_front_matter): """ 更新 Markdown 文件中的 front-matter 内容。 """ with open(file_path, 'r+', encoding='utf-8') as file: content = file.read() new_front_matter_str = yaml.dump(new_front_matter, default_flow_style=False) content = re.sub(r'---\n(.*?)\n---\n', f'---\n{new_front_matter_str}\n---\n', content, flags=re.DOTALL) with open(file_path, 'w', encoding='utf-8') as file: file.write(content)
def extract_front_matter_from_dir(directory_path): """ 遍历指定目录及其子目录下的所有 .md 文件,提取它们的 front-matter 内容,并将所有内容添加到列表中。 """ front_matter_list = [] for root, dirs, files in os.walk(directory_path): for filename in files: file_path = os.path.join(root, filename) if filename.endswith('.md'): front_matter = extract_front_matter(file_path) if front_matter: front_matter_list.append(front_matter) return front_matter_list
def generate_unique_10digit_numbers(existing_numbers, n): """ 生成 n 个不在 existing_numbers 列表中的 10 位数字。 :param existing_numbers: 已存在的整数列表 :param n: 需要生成的数字数量 :return: 不重复的 10 位数字列表 """ unique_numbers = set(existing_numbers) generated_numbers = [] if len(unique_numbers) > 9999999999 - 1000000000: return None while len(generated_numbers) < n: num = random.randint(1000000000, 9999999999) if num not in unique_numbers: generated_numbers.append(num) unique_numbers.add(num) return generated_numbers
def write_int_list_to_md(file_path, int_list): """ 将整数列表的成员按行写入一个Markdown文件。 :param file_path: Markdown文件的路径 :param int_list: 要写入文件的整数列表 """ with open(file_path, 'w', encoding='utf-8') as file: for number in int_list: file.write(f"{number}\n")
if __name__ == "__main__": file_path = MD_FILE_PATH front_matter_list = extract_front_matter_from_dir(file_path) if not front_matter_list: print("没有找到 front-matter 或目录为空。") os.abort() abbrlink_list = [] for fm in front_matter_list: if 'abbrlink' not in fm: print(f"ERR! abbrlink not in {fm}") continue link = int(fm['abbrlink']) if link in abbrlink_list: print(f"ERR! {link} in abbrlink list!") continue abbrlink_list.append(link) new_abbrlink = generate_unique_10digit_numbers(abbrlink_list, NEW_ABBRLINK_SIZE) for link in new_abbrlink: print(link) print("Gen abbrlink success") write_int_list_to_md(NEW_ABBRLINK_MD_FILE, new_abbrlink) print("Write abbrlink to", NEW_ABBRLINK_MD_FILE)
|
脚本运行效果如下,会生成新的 abbrlink 链接数字,然后写入到指定的 md 文件中。这样在 obsidian 里面就能看到这个 md 文件,取用里面的 abbrlink 了。用完了之后再手动执行一下脚本更新 abbrlink 就完事啦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ❯ python3 gen_abbrlink.py 8608489065 7885829874 8484489314 4284761477 1589125738 9151131777 4800824161 7141292217 2714461943 5131440419 2816690027 9574459795 6572894529 2920325088 2724835080 7631222809 1802821635 3120273636 2860205445 3100823185 Gen abbrlink success Write abbrlink to ../../Notes/ABBRLINK归档.md
|
3.2. Github Action 配置
接下来就是配置 Github Action 来同步两个仓库了。让 GPT 写了个大概,发现 GPT 在瞎说,它给出 https 的仓库 clone 链接,并表示用自带的 GITHUB_TOKEN
就能克隆私有仓库了,但实际上完全没用。最后还是得用老办法 ssh 密钥对来实现。
首先使用如下命令生成一个 ssh 密钥,弹出的提示中填写一个文件名字(不然会覆盖默认目录的 ssh 密钥对)
1
| ssh-keygen -t rsa -C "github action"
|
然后,搞清楚同步的方向,我的需要是将 obsidian 仓库中的内容同步到 hexo 仓库,所以公钥放在 hexo 仓库,私钥放在 obsidian 仓库中。
在 hexo 仓库(被推送的仓库)中,仓库设置 Settings->Deploy keys->Add deploy key
添加公钥,命名为 HEXO_PUB_KEY
。注意需要勾选允许 write 写入仓库,不然默认权限只允许 pull 和 clone 仓库。
在 obsidian 仓库中,仓库设置 Settings->Secrets and variables->Secrets
添加私钥,命名为 HEXO_PRI_KEY
。
最后的 Github Action Workflow 文件如下,将该文件写入 obsidian 仓库的.github/workflows/sync-code-to-posts.yml
即可,每个步骤都写了注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| name: Sync CODE to _posts
on: push: paths: - 'Notes/CODE/**'
jobs: sync: runs-on: ubuntu-latest
steps: - name: Checkout muob repository uses: actions/checkout@v3
- name: Set up Git env: ACTIONS_KEY: ${{ secrets.HEXO_PRI_KEY }} run: | mkdir -p ~/.ssh/ echo "$ACTIONS_KEY" > ~/.ssh/id_rsa chmod 700 ~/.ssh chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com >> ~/.ssh/known_hosts git config --global user.name "musnows" git config --global user.email "ezplayingd@126.com" git config --global core.quotepath false git config --global i18n.commitEncoding utf-8 git config --global i18n.logOutputEncoding utf-8 - name: Checkout HexoBlog repository run: | git clone git@github.com:musnows/Hexo-Blog.git HexoBlog - name: Sync files from CODE to _posts run: | rsync -av --delete Notes/CODE/ HexoBlog/source/_posts/ - name: Commit and push changes to HexoBlog repository run: | cd HexoBlog git add . git commit -m "Sync CODE to _posts at $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M:%S')" git push origin hexo
|
第二步的 Git 操作中,我们将仓库配置的 secrets.HEXO_PRI_KEY
映射成环境变量 ACTIONS_KEY
,然后写入执行 action 的 ubuntu 环境的 ~/.ssh/id_rsa
私钥文件中,这样就能操作另外一个仓库了。
第四步的 Sync 操作使用了 rsync 命令
1
| rsync -av --delete 源文件夹 目标文件夹
|
解释一下这里的几个命令参数,-a
用于保持文件的原有属性,-v
代表 verbose,会输出详细的日志,--delete
用于在目标目录中删除源目录中不存在的文件(同步删除操作)。
4. 测试效果
我在目录中创建了一个测试文件,push 到了远端仓库中,触发了 action

hexo 配置仓库成功被 push 了,有更新的文件也是在 obsidain 仓库中被修改的文件,符合预期。

这样就搞定啦!以后我只需要 push 博客到 obsidian 仓库中,就能自动同步到 hexo 仓库内了。不需要手动做那部分繁琐的操作