使用 Github Action 自动同步 obisidian 和 hexo 仓库,避免手动操作。

1. 烦恼

先来说说慕雪现在的笔记和博客是怎么管理的吧,我正在使用两套笔记软件

  • 思源笔记:私密性高一些,不是博客的笔记都在这里面。由于思源笔记不是 markdown 编辑器,不能直接和 hexo 对接;
  • obisdian:专门管理 hexo 的博客;

然后我的 hexo 博客和 obsidian 又有分离,hexo 配置仓库是一个单独的 git 仓库(后文简称为 hexo 仓库),obsidian 博客库也是一个单独的 git 仓库(后文简称为 obisidian 仓库)。

我采用的操作特别繁琐,步骤如下:

  1. 在 obsidian 里面写好博客之后,手动使用 FreeFileSync 软件,将 obisidian/blog 目录同步到 hexo/source/_posts 目录中(这两个目录完全一样);
  2. 然后再到 hexo 本地仓库中执行 hexo 三板斧命令,给新的博客生成 abbrlink,push 到 hexo 的 github 仓库;
  3. 再用 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,启动!

python
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
# 生成不冲突的abbrlink
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()

# 使用正则表达式匹配 front-matter
match = re.match(r'---\n(.*?)\n---\n', content, re.DOTALL)

if match:
front_matter = match.group(1)
return yaml.safe_load(front_matter) # 使用 yaml 解析 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()

# 使用正则表达式去除 front-matter
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()

# 使用正则表达式替换 front-matter
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)

# 只处理 .md 文件
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) # 随机生成一个10位数字

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__":
# 假设 markdown 文件目录路径为 "../../Notes/CODE"
file_path = MD_FILE_PATH

# 提取所有 .md 文件的 front-matter
front_matter_list = extract_front_matter_from_dir(file_path)

# 打印所有文件的 front-matter
if not front_matter_list:
print("没有找到 front-matter 或目录为空。")
os.abort()

abbrlink_list = []
for fm in front_matter_list:
# print(f"文件的 front-matter 内容:", fm)
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)

# 获取新的abbrlink
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 就完事啦。

plaintext
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 密钥对)

bash
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 即可,每个步骤都写了注释。

yaml
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/**' # 监听 CODE 文件夹内的文件变化,没有变化不会触发action

jobs:
sync:
runs-on: ubuntu-latest

steps:
# 检出 obsidian 仓库的代码
- name: Checkout muob repository
uses: actions/checkout@v3

# 设置 Git 配置
- 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
# 克隆 HexoBlog 仓库(私有仓库),使用 ssh 来进行认证
- name: Checkout HexoBlog repository
run: |
git clone git@github.com:musnows/Hexo-Blog.git HexoBlog
# 同步文件:将 obsidain 仓库中的 CODE 文件夹内容复制到 HexoBlog 仓库的 _posts 文件夹
- name: Sync files from CODE to _posts
run: |
rsync -av --delete Notes/CODE/ HexoBlog/source/_posts/
# 提交更改并推送到 HexoBlog 仓库
- 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 命令

bash
1
rsync -av --delete 源文件夹 目标文件夹

解释一下这里的几个命令参数,-a 用于保持文件的原有属性,-v 代表 verbose,会输出详细的日志,--delete 用于在目标目录中删除源目录中不存在的文件(同步删除操作)。

4. 测试效果

我在目录中创建了一个测试文件,push 到了远端仓库中,触发了 action

image.png

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

image.png

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