因为我自己本身也有搜集高质量信源以观察市场的需求+该死的英一对词汇量和翻译能力的要求比较高,我决定开始系统性地精读外文刊物。
但……你们知道的,我是懒狗,有没有一种优雅的方法将这些外刊内容同步到我的阅读设备(比如我的 MatePad)上,并且方便我随时打开进行阅读、划词翻译和做笔记,而无需经历繁琐的文件传输和格式转换?
——目光转向Obsidian
没错,就是那个强大的、基于本地 Markdown 文件的双链笔记软件。因为Obsidian基于本地文件夹结构,我们可以利用各种第三方同步工具实现免费或自托管的同步方案,只要你配置好了多端同步(我是用的是腾讯云cos),那你近乎可以在所有平台体验到完全相同的阅读/笔记体验。
Obsidian 看起来能完美解决我的阅读和笔记需求。但最开始的问题——“懒”,还没解决。我总不能每天手动去各大外刊网站寻找、下载文章,再复制粘贴或者转换格式,最后放到 Obsidian 的库(Vault)里吧?这不仅繁琐,而且一点也不“优雅”。
好,我们来继续!
我偶然发现 awesome-english-ebooks 这个Github库会在每周五定期更新经济学人、纽约客等英语外刊杂志的PDF/Mobi/Epub文件,这个仓库解决了内容来源的问题,而且更新频率稳定。这意味着,理论上,我只需要找到一种方法,在每周五这个仓库更新后,自动抓取最新的期刊文件,然后把它们放到我的 Obsidian (对应云端的s3 cos)库里,就可以实现全自动化的外刊抓取与阅读了。
这就是 Github Actions 发挥作用的地方。
Github Actions 是 GitHub 提供的一项持续集成和持续部署(CI/CD)服务,但它的能力远不止于此。我们可以利用它来自动化几乎任何与代码仓库相关的任务,包括我们现在这个需求:定时检查外部资源、下载文件、并将这些文件提交到我们自己的仓库中。
我的设想流程是这样的:
设置一个定时触发器:利用 Github Actions 的 schedule 功能,让工作流(Workflow)每周(比如周五晚上或周六早上)自动运行一次。
拉取最新外刊:工作流启动后,它会去 awesome-english-ebooks 仓库,把最新的期刊文件(比如我主要关心的《经济学人》的 PDF 文件)下载到 Action 的运行环境中。
处理和组织文件(可选):可以根据需要对下载的文件进行重命名,或者按照期刊名称、日期创建好文件夹结构,让它们在 Obsidian 中看起来更整洁。
将文件推送到“Obsidian 仓库”:这是关键一步。为了让 Github Actions 能够管理我们 Obsidian 的内容,我选择直接让Action把对应的PDF文件上传到S3储存桶的指定文件夹里,由此实现了巧妙的同步。
触发同步:当 Github Actions 将新的外刊文件 push 到“Obsidian 仓库”后,本地设备上配置的同步方案(无论是基于 Git 的同步插件如 Obsidian Git,还是像腾讯云 COS + FolderSync/Syncthing 等)就会检测到云端(即 Github 仓库)的变化,并将这些新文件自动拉取到你的 MatePad 等设备上的 Obsidian Vault 中。
最终效果:
每周什么都不用做,最新的《经济学人》(或其他你选择的刊物)就会自动出现在我所有设备的 Obsidian 中,随时可以打开阅读、划词、做笔记。下载、传输、整理这些繁琐的步骤完全消失,真正实现了“懒人”的优雅阅读。
为了复现我的 Workflow,你需要准备:
- 一个Github账号
- 一个S3储存桶账号(腾讯云阿里云Cloudflare都有,选择太多了)
- Obsidian笔记
- 一点点的耐心
一、Obsidian设置
为了让 Github Actions 下载的外刊能够自动出现在我们的 Obsidian 中,我们需要先打通 Obsidian 与云存储之间的同步通道。这里我们选用一个广受好评的第三方同步插件:Remotely Save。
1. 安装 Remotely Save 插件:
- 打开你的 Obsidian。
- 进入 设置 (Settings) -> 第三方插件 (Community Plugins)。
- 重要提示: 如果你之前没有启用过社区插件,需要先关闭 安全模式 (Safe Mode)。请知晓其中的风险,只安装你信任的插件。
- 点击 浏览 (Browse) 社区插件市场。
- 搜索 Remotely Save。
- 找到插件后,点击 安装 (Install),安装完成后点击 启用 (Enable)。
2. 配置 Remotely Save 连接 S3 服务:
安装并启用后,Remotely Save 的设置选项会出现在左侧边栏的 第三方插件 区域。点击进入其设置界面。
选择远程服务: 在插件设置中,找到选择远程服务提供商的选项。这里你需要选择 S3 或者专门列出的 Tencent COS (如果插件有此区分选项)。
填写 S3 凭证: 这是最关键的一步。你需要填入连接到你云存储桶所需的信息。具体设置界面可能因插件版本略有不同,但核心信息是相同的。 你可以参考下图所示的配置界面:
获取必要的 S3 凭证信息: 你需要登录你的云服务提供商控制台(以腾讯云对象存储 COS 为例)获取以下四个关键信息。请务必妥善保管,尤其是 SecretKey!
- COS_SECRET_ID (对应 S3 的 Access Key ID): 访问密钥 ID,用于标识你的账户。
- COS_SECRET_KEY (对应 S3 的 Secret Access Key): 访问密钥 Key,这是高度敏感信息,请勿泄露! 它与 Secret ID 配合使用,用于验证你的请求。
- COS_REGION (对应 S3 的 Region): 你的存储桶所在的区域。例如,北京区域通常是 ap-beijing。请确保填写插件要求的正确格式。
- COS_BUCKET (对应 S3 的 Bucket Name): 你创建的用于存储 Obsidian 库文件的存储桶名称。
填写到插件设置中: 将上面获取到的四个值,准确无误地填写到 Remotely Save 插件设置界面中对应的字段里(例如:Endpoint/Server Address 可能需要根据 COS_REGION 和 COS_BUCKET 构造,或者有单独的 Region 和 Bucket 字段,具体请参照插件说明或界面提示)。 配置完成后,可以尝试点击插件设置中的 检查 (Check) 或 同步 (Sync/Save) 按钮,看是否能成功连接并同步少量文件到你的 S3 存储桶。
二、GitHub设置
现在,我们已经配置好了 Obsidian 通过 Remotely Save 与 S3 云存储同步的通道。接下来,我们需要在 GitHub 上设置一个自动化流程,让它定期从源仓库拉取最新的外刊,并将这些文件推送到我们之前配置好的 S3 存储桶中。
1.Fork源仓库
我们的外刊内容来源于 hehonghui/awesome-english-ebooks 这个优秀的仓库。但我们不能直接在这个仓库上运行我们自己的自动化任务,也无法直接修改它。因此,我们需要先 Fork 这个仓库
- 什么是 Fork? Forking 操作会在你的 GitHub 账户下创建一个源仓库的完整副本。这个副本完全属于你,你可以自由地修改代码、添加文件(比如我们的 Workflow 文件),而不会影响到原始仓库。
- 如何操作?
- 访问 https://github.com/hehonghui/awesome-english-ebooks
- 登录你的 GitHub 账号。
- 点击页面右上角的 Fork 按钮。
- 选择你自己的 GitHub 账户作为目标位置,并确认 Fork。
- 结果: 完成后,你的 GitHub 账号下就会出现一个名为 awesome-english-ebooks 的新仓库(地址类似 https://github.com/<你的用户名>/awesome-english-ebooks)。后续所有的操作都将在你 Fork 后的这个仓库中进行。
2.创建Workflow文件:定义自动化任务
GitHub Actions 的核心是 Workflow 文件。这是一个 YAML 格式的文本文件,用来定义自动化任务的触发条件、执行步骤和所需环境。
- 存放位置: GitHub Actions 要求 Workflow 文件必须存放在仓库根目录下的 .github/workflows/ 文件夹内。你需要先在你的 Fork 后的仓库 中创建这个目录结构。
- 创建文件:
- 方法一 (推荐,通过 GitHub 网页界面):
- 进入你 Fork 后的 awesome-english-ebooks 仓库页面。
- 点击上方的 Actions 标签页。
- 如果仓库还没有 Workflow,GitHub 可能会提示你创建。你可以点击 set up a workflow yourself (或者类似的链接)。
- 这会直接带你到创建新文件的界面,路径已经预设为 .github/workflows/。
- 如果你采用我的防范,那么你需要创建两个yml文件,分别是sync-upstream.yml和sync-to-obsidian.yml。前者负责自动和上游仓库同步,后者负责推送文件到Obsidian库。
- 方法二 (本地操作): 不推荐该方法,因为原始仓库过大
- 将你 Fork 后的仓库克隆 (clone) 到本地。
- 在本地仓库的根目录下创建 .github 文件夹,再在其中创建 workflows 文件夹。
- 在 workflows 文件夹内创建一个新的 YAML 文件 (例如 sync-periodicals.yml)。
- 稍后编辑完文件内容后,通过 git add, git commit, git push 将这个文件推送到你的 GitHub 仓库。
- 方法一 (推荐,通过 GitHub 网页界面):
- 文件内容: 接下来的一步,我们将在这个 YAML 文件中编写具体的指令,告诉 GitHub Actions 要做什么。现在,你只需要先创建好这个空文件即可。
3.配置安全凭证 (GitHub Secrets):保护你的 S3 访问密钥
我们的 Workflow 需要访问你的 S3 存储桶(例如腾讯云 COS)才能上传文件。这就需要用到我们在第一步中获取的四个凭证信息 (COS_SECRET_ID, COS_SECRET_KEY, COS_REGION, COS_BUCKET)。
极其重要: 绝对不能 将这些敏感信息直接写在 Workflow 文件 (YAML) 中!因为 Workflow 文件是代码仓库的一部分,会被公开(即使是私有仓库,也存在泄露风险)。
正确的做法是使用 GitHub Secrets。Secrets 是 GitHub 提供的用于存储敏感信息(如 API 密钥、密码、访问令牌等)的功能,它们会被加密存储,并且只在 Action 运行时作为环境变量注入,不会在日志或代码中暴露。
- 如何设置 Secrets:
- 进入你 Fork 后的 awesome-english-ebooks 仓库页面。
- 点击上方的 Settings 标签页。
- 在左侧导航栏中,找到 Secrets and variables,点击展开,然后选择 Actions。
- 在 Repository secrets 部分,点击 New repository secret 按钮。
- 你需要 依次创建四个 Secret:
- Name: COS_SECRET_ID
Secret: 粘贴你之前获取的腾讯云 SecretId 值。 - Name: COS_SECRET_KEY
Secret: 粘贴你之前获取的腾讯云 SecretKey 值。 (再次强调,这是最敏感的信息!) - Name: COS_REGION
Secret: 粘贴你的腾讯云 COS 区域,例如 ap-beijing。 - Name: COS_BUCKET
Secret: 粘贴你的腾讯云 COS 存储桶名称。
- Name: COS_SECRET_ID
- 确保 Secret 名称完全匹配 (COS_SECRET_ID, COS_SECRET_KEY, COS_REGION, COS_BUCKET),因为我们稍后会在 Workflow 文件中通过这些名称来引用它们。仔细检查拼写和大小写。
4.Github Action YAML文件
当然,你也可以直接忽视下面这一坨文件,直接Fork 我的仓库,并按照上文第三小节的步骤修改自己的环境变量来进行同步。
4.1 定期同步上游仓库:sync-upstream.yml
这个文件的核心目标是:确保你 Fork 的仓库能定期自动拉取 awesome-english-ebooks 源仓库的最新更新。 这样,当源仓库发布新一期杂志时,你的仓库也能及时获取到。 它的工作流程大致如下:
- 定时触发与手动触发 (on):
- 它被设置为每周五的特定时间(UTC 16:00,对应北京时间周六 00:00)自动运行。这个时间点通常在源仓库更新之后,确保能拉取到最新的内容。
- 同时,它也允许 workflow_dispatch,意味着你可以随时在 GitHub 仓库的 Actions 页面手动点击运行此流程,方便测试或在非预定时间更新。
- 执行环境 (jobs: sync):
- 指定在最新的 Ubuntu 虚拟机环境中运行。
- 关键步骤 (steps):
- 检出当前仓库 (actions/checkout): 首先,将你 自己 Fork 后的仓库代码下载到运行环境中。使用了 fetch-depth: 1 进行浅克隆,只获取最新版本历史,节省时间和资源。
- 添加上游仓库 (git remote add upstream… & git fetch upstream…): 将原始的 awesome-english-ebooks 仓库添加为一个名为 upstream 的远程源,并拉取其最新的提交信息(同样使用浅层拉取)。
- 备份 GitHub Action 文件: 这是一个重要的保险步骤。因为接下来的合并操作可能会覆盖你仓库中的 .github/workflows 文件夹(如果上游仓库恰好也有同名文件),所以这里先把这个文件夹备份到临时目录。
- 合并上游仓库的更改 (git merge upstream/…): 尝试将从 upstream 拉取下来的最新更改合并到你当前的分支(通常是 master 或 main)。这一步就是将新的杂志文件同步到你的仓库副本中的关键操作。代码中包含了处理潜在分支名差异 (master vs main) 和自动设置提交者信息的逻辑。
- 恢复 GitHub Action 文件: 将之前备份的 Workflow 文件复制回 .github/workflows 目录,并提交这个更改(如果确实有恢复操作)。这确保了即使合并时上游仓库有冲突或修改,你自己的自动化脚本不会丢失。
- 推送更改到你的仓库 (git push origin): 最后,将所有本地合并、恢复后的更改推送回你自己的 GitHub 远程仓库。至此,你的 Fork 就包含了最新的外刊文件。 小结:这个 Workflow 的职责是“进货”,它定期检查源头,把最新的杂志文件同步到你的 GitHub 仓库中。 正式代码:
name: 同步上游仓库
on:
schedule:
# 每周六0点运行(UTC时间是每周五16:00,对应北京时间24:00,也就是周六00:00)
- cron: '0 16 * * 5'
# 允许手动触发工作流
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
# 检出当前仓库代码(增加深度以获取更完整的历史记录)
- name: 检出当前仓库
uses: actions/checkout@v3
with:
fetch-depth: 0
# 添加上游仓库并获取更完整的历史记录
- name: 添加上游仓库
run: |
git remote add upstream https://github.com/hehonghui/awesome-english-ebooks.git
git fetch upstream --depth=50
# 备份GitHub Action文件
- name: 备份GitHub Action文件
run: |
mkdir -p /tmp/github-actions-backup
if [ -d ".github/workflows" ]; then
cp -r .github/workflows /tmp/github-actions-backup/
fi
# 检测上游仓库的默认分支
- name: 检测上游仓库的分支
id: detect-branch
run: |
if git ls-remote --heads upstream main | grep main; then
echo "UPSTREAM_BRANCH=main" >> $GITHUB_ENV
else
echo "UPSTREAM_BRANCH=master" >> $GITHUB_ENV
fi
if git rev-parse --verify --quiet main; then
echo "LOCAL_BRANCH=main" >> $GITHUB_ENV
else
echo "LOCAL_BRANCH=master" >> $GITHUB_ENV
fi
# 合并上游仓库的更改
- name: 合并上游仓库的更改
run: |
# 确保我们在正确的本地分支上
git checkout ${{ env.LOCAL_BRANCH }}
# 配置Git用户信息(在合并前设置)
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "GitHub Actions"
# 使用--allow-unrelated-histories参数合并上游仓库的更改
git merge upstream/${{ env.UPSTREAM_BRANCH }} --allow-unrelated-histories -m "同步上游仓库更改" || {
echo "合并冲突,以上游版本为准"
git reset --hard upstream/${{ env.UPSTREAM_BRANCH }}
}
# 恢复GitHub Action文件
- name: 恢复GitHub Action文件
run: |
if [ -d "/tmp/github-actions-backup/workflows" ]; then
mkdir -p .github/workflows
cp -r /tmp/github-actions-backup/workflows/* .github/workflows/
git add .github/workflows
git commit -m "恢复GitHub Action文件" || echo "没有需要恢复的文件"
fi
# 推送更改到你的仓库
- name: 推送更改到仓库
run: |
git push origin ${{ env.LOCAL_BRANCH }}
4.2 推送文件到Obsidian库:sync-to-obsidian.yml
这个文件的核心目标是:在第一个 Workflow 成功同步了最新的外刊文件后,自动找出我们关心的那几本杂志的最新 PDF 文件,并将它们上传到你之前配置好的腾讯云 COS (或其他 S3) 存储桶的指定位置。 它的工作流程设计如下:
- 触发条件 (on):
- 主要触发器 (workflow_run): 它被配置为在上一个 同步上游仓库 Workflow 成功完成后自动触发。这是一种链式反应,确保只有在“进货”成功后,才进行“上架”操作。
- 手动触发 (workflow_dispatch): 同样允许手动触发,方便调试或单独执行上传操作。
- 运行条件 (jobs: sync-to-cos -> if):
- 增加了一个检查:只有当触发它的 同步上游仓库 工作流结论是 success (成功),或者当它是被手动触发时,这个 Job 才会真正运行。防止在上游同步失败的情况下执行无效的上传。
- 关键步骤 (steps):
- 检出当前仓库 (actions/checkout): 再次检出你仓库的代码。但这次使用了 sparse-checkout (稀疏检出) 的高级功能。它只下载仓库中你明确指定的几个包含外刊的目录(如 01_economist, 02_new_yorker 等),而不是整个仓库。这极大地提高了效率,尤其当仓库历史越来越大时。
- 安装 AWS CLI: 安装 AWS 命令行工具。虽然我们用的是腾讯云 COS,但 COS 兼容 S3 API,所以可以使用 AWS CLI 来操作。
- 配置 AWS 凭证: 这是连接到你 S3 存储桶的关键。它在运行环境中动态创建 AWS 配置文件 (~/.aws/credentials 和 ~/.aws/config)。这里安全地使用了你之前在 GitHub Secrets 中存储的 COS_SECRET_ID, COS_SECRET_KEY, COS_REGION 和 COS_BUCKET。注意,这里配置了腾讯云 COS 的特定 endpoint_url,让 AWS CLI 知道要连接的不是 AWS S3 而是腾讯云 COS。你的密钥绝不会暴露在代码或日志中。
- 获取文件更新日期: 获取形如 YYYY.MM.DD 的刊物更新日期,存为一个变量,用于后续在 S3 中创建按日期组织的文件夹。
- 列出目录内容: 一个辅助步骤,用于在 Action 日志中打印出检出的目录内容,方便调试时查看文件是否按预期存在。
- 查找最新的杂志文件 (多个步骤): 针对你关心的每种刊物(经济学人、纽约客、大西洋月刊、连线),使用 shell 命令 (find, sort, head) 来定位其对应目录下最新创建的子目录。脚本假设最新的期刊文件存放在最新命名的子目录中。找到的目录路径会保存为输出变量(如 steps.economist.outputs.latest_dir)。
- 同步各刊物到 COS (多个步骤): 这是最终的上传环节。
- 条件执行 (if): 每个上传步骤都先检查上一步是否成功找到了对应的最新目录。
- 遍历 PDF 文件: 在找到的最新期刊目录中,脚本会遍历查找所有的 .pdf 文件。
- 上传 (aws s3api put-object): 对找到的每个 PDF 文件,执行 aws s3api put-object 命令进行上传。
- —bucket: 指定你的 COS 存储桶名称 (来自 Secret)。
- —key: 这是文件在存储桶中的最终路径和名称。它被精心构造成 2.外刊/<刊物名称>/<当前日期>/<原始文件名>.pdf 的格式。例如 2.外刊/经济学人/2023.10.27/The Economist - October 27 2023.pdf。这个结构化的路径将直接映射到你 Obsidian 中通过 Remotely Save 同步后的文件夹结构,非常清晰。
- —body: 指定要上传的本地 PDF 文件。
- —endpoint-url: 再次确保命令指向腾讯云 COS。
小结:这个 Workflow 是“上架”机器人。它在上一个 Workflow 完成后启动,智能地找到你关心的最新 PDF 期刊,然后按照预设的路径结构将它们精准地上传到你的 S3 云存储中,等待 Remotely Save 将它们同步到你的 Obsidian。 正式代码:
name: 同步外刊到Obsidian
on:
# 在同步上游仓库工作流完成后运行
workflow_run:
workflows: ["同步上游仓库"]
types:
- completed
# 允许手动触发
workflow_dispatch:
jobs:
sync-to-cos:
runs-on: ubuntu-latest
# 只有当触发的工作流成功完成时才运行
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
# 检出当前仓库代码(使用稀疏检出策略,只检出必要的目录结构)
- name: 检出当前仓库
uses: actions/checkout@v3
with:
fetch-depth: 1
sparse-checkout: |
01_economist
02_new_yorker
04_atlantic
05_wired
sparse-checkout-cone-mode: true
# 安装AWS CLI
- name: 安装AWS CLI
uses: unfor19/install-aws-cli-action@v1
with:
version: 2
# 配置AWS凭证(用于访问腾讯云COS)
- name: 配置AWS凭证
run: |
mkdir -p ~/.aws
cat > ~/.aws/credentials << EOF
[default]
aws_access_key_id = ${{ secrets.COS_SECRET_ID }}
aws_secret_access_key = ${{ secrets.COS_SECRET_KEY }}
EOF
cat > ~/.aws/config << EOF
[default]
region = ${{ secrets.COS_REGION }}
output = json
s3 =
addressing_style = virtual
endpoint_url = https://cos.${{ secrets.COS_REGION }}.myqcloud.com
EOF
# 验证配置是否生效
echo "AWS CLI配置完成,当前配置:"
aws configure list
# 列出目录内容,以便于调试
- name: 列出目录内容
run: |
echo "列出经济学人目录内容:"
ls -la ./01_economist || echo "经济学人目录不存在"
echo "列出纽约客目录内容:"
ls -la ./02_new_yorker || echo "纽约客目录不存在"
echo "列出Atlantic目录内容:"
ls -la ./04_atlantic || echo "Atlantic目录不存在"
echo "列出Wired目录内容:"
ls -la ./05_wired || echo "Wired目录不存在"
# 查找最新的经济学人文件
- name: 查找最新的经济学人文件
id: economist
run: |
if [ -d "./01_economist" ]; then
LATEST_ECONOMIST_DIR=$(find ./01_economist -maxdepth 1 -type d -name "te_*" | sort -r | head -n 1)
echo "latest_dir=$LATEST_ECONOMIST_DIR" >> $GITHUB_OUTPUT
# 从目录名中提取日期(格式如te_2025.04.05)
if [ -n "$LATEST_ECONOMIST_DIR" ]; then
DIR_NAME=$(basename "$LATEST_ECONOMIST_DIR")
ECONOMIST_DATE=$(echo "$DIR_NAME" | sed 's/te_//')
echo "date=$ECONOMIST_DATE" >> $GITHUB_OUTPUT
echo "找到最新的经济学人目录: $LATEST_ECONOMIST_DIR,日期: $ECONOMIST_DATE"
fi
else
echo "经济学人目录不存在"
echo "latest_dir=" >> $GITHUB_OUTPUT
fi
# 查找最新的纽约客文件
- name: 查找最新的纽约客文件
id: newyorker
run: |
if [ -d "./02_new_yorker" ]; then
LATEST_NEWYORKER_DIR=$(find ./02_new_yorker -maxdepth 1 -type d -mindepth 1 -not -path "*/\.*" | sort -r | head -n 1)
echo "latest_dir=$LATEST_NEWYORKER_DIR" >> $GITHUB_OUTPUT
# 从目录名中提取日期
if [ -n "$LATEST_NEWYORKER_DIR" ]; then
DIR_NAME=$(basename "$LATEST_NEWYORKER_DIR")
echo "date=$DIR_NAME" >> $GITHUB_OUTPUT
echo "找到最新的纽约客目录: $LATEST_NEWYORKER_DIR,日期: $DIR_NAME"
fi
else
echo "纽约客目录不存在"
echo "latest_dir=" >> $GITHUB_OUTPUT
fi
# 同步经济学人到COS
- name: 同步经济学人到COS
if: steps.economist.outputs.latest_dir != ''
run: |
echo "配置AWS S3寻址样式"
aws configure set default.s3.addressing_style virtual
echo "开始同步经济学人目录: ${{ steps.economist.outputs.latest_dir }}"
if [ -d "${{ steps.economist.outputs.latest_dir }}" ]; then
# 只上传PDF文件
for file in "${{ steps.economist.outputs.latest_dir }}"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/经济学人/${{ steps.economist.outputs.date }}/$filename"
# 检查文件是否已存在
echo "检查COS中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.COS_BUCKET }} --key "$s3_key" --endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 获取文件大小
file_size=$(stat -c%s "$file")
echo "文件大小: $file_size 字节"
# 直接使用低级命令上传
echo "使用s3api上传文件..."
aws s3api put-object \
--bucket ${{ secrets.COS_BUCKET }} \
--key "$s3_key" \
--body "$file" \
--content-length $file_size \
--endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步经济学人PDF到COS"
else
echo "经济学人目录不存在或为空: ${{ steps.economist.outputs.latest_dir }}"
fi
# 同步纽约客到COS
- name: 同步纽约客到COS
if: steps.newyorker.outputs.latest_dir != ''
run: |
if [ -d "${{ steps.newyorker.outputs.latest_dir }}" ]; then
# 只上传PDF文件
for file in "${{ steps.newyorker.outputs.latest_dir }}"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/纽约客/${{ steps.newyorker.outputs.date }}/$filename"
# 检查文件是否已存在
echo "检查COS中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.COS_BUCKET }} --key "$s3_key" --endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 获取文件大小
file_size=$(stat -c%s "$file")
echo "文件大小: $file_size 字节"
# 直接使用低级命令上传
echo "使用s3api上传文件..."
aws s3api put-object \
--bucket ${{ secrets.COS_BUCKET }} \
--key "$s3_key" \
--body "$file" \
--content-length $file_size \
--endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步纽约客PDF到COS"
else
echo "纽约客目录不存在或为空: ${{ steps.newyorker.outputs.latest_dir }}"
fi
# 同步The Atlantic到COS(如果有更新)
- name: 同步The Atlantic到COS
run: |
if [ -d "./04_atlantic" ]; then
LATEST_ATLANTIC_DIR=$(find ./04_atlantic -maxdepth 1 -type d -mindepth 1 -not -path "*/\.*" | sort -r | head -n 1)
if [ -n "$LATEST_ATLANTIC_DIR" ] && [ -d "$LATEST_ATLANTIC_DIR" ]; then
# 从目录名中提取日期
DIR_NAME=$(basename "$LATEST_ATLANTIC_DIR")
ATLANTIC_DATE=$DIR_NAME
echo "找到最新的大西洋月刊目录: $LATEST_ATLANTIC_DIR,日期: $ATLANTIC_DATE"
# 只上传PDF文件
for file in "$LATEST_ATLANTIC_DIR"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/大西洋月刊/$ATLANTIC_DATE/$filename"
# 检查文件是否已存在
echo "检查COS中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.COS_BUCKET }} --key "$s3_key" --endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 获取文件大小
file_size=$(stat -c%s "$file")
echo "文件大小: $file_size 字节"
# 直接使用低级命令上传
echo "使用s3api上传文件..."
aws s3api put-object \
--bucket ${{ secrets.COS_BUCKET }} \
--key "$s3_key" \
--body "$file" \
--content-length $file_size \
--endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步The Atlantic PDF到COS"
else
echo "未找到有效的The Atlantic目录"
fi
else
echo "Atlantic目录不存在"
fi
# 同步Wired到COS(如果有更新)
- name: 同步Wired到COS
run: |
if [ -d "./05_wired" ]; then
LATEST_WIRED_DIR=$(find ./05_wired -maxdepth 1 -type d -mindepth 1 -not -path "*/\.*" | sort -r | head -n 1)
if [ -n "$LATEST_WIRED_DIR" ] && [ -d "$LATEST_WIRED_DIR" ]; then
# 从目录名中提取日期
DIR_NAME=$(basename "$LATEST_WIRED_DIR")
WIRED_DATE=$DIR_NAME
echo "找到最新的连线杂志目录: $LATEST_WIRED_DIR,日期: $WIRED_DATE"
# 只上传PDF文件
for file in "$LATEST_WIRED_DIR"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/连线杂志/$WIRED_DATE/$filename"
# 检查文件是否已存在
echo "检查COS中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.COS_BUCKET }} --key "$s3_key" --endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 获取文件大小
file_size=$(stat -c%s "$file")
echo "文件大小: $file_size 字节"
# 直接使用低级命令上传
echo "使用s3api上传文件..."
aws s3api put-object \
--bucket ${{ secrets.COS_BUCKET }} \
--key "$s3_key" \
--body "$file" \
--content-length $file_size \
--endpoint-url=https://cos.${{ secrets.COS_REGION }}.myqcloud.com
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步Wired PDF到COS"
else
echo "未找到有效的Wired目录"
fi
else
echo "Wired目录不存在"
fi
三、Cloudflare R2 配置
在方法发布后,很多朋友发现Cloudflare R2没办法简单修改使用,我自己改一晚上bug满头包才发现是因为AWS CLI的较新版本(2.23.0和1.37.0以上)引入了默认校验和行为的修改,和Cloudflare R2的API不兼容才导致没办法直接套用的,令人感慨。
在腾讯云COS脚本的基础上,我将 AWS CLI 版本降级到已知与 Cloudflare R2 兼容的版本 2.22.35,在 在所有的 aws s3 cp
命令中添加了 --checksum-algorithm CRC32
参数,现在运行脚本之后已经可以正常上传了。 效果图:
如果你想要使用R2,需要在github仓库里配置四个环境变量
- R2_ACCOUNT_ID
- R2_BUCKET_NAME
- R2_ACCESS_KEY_ID
- R2_SECRET_ACCESS_KEY
当然,你也可以直接fork我的仓库
最新的支持R2的脚本如下:
name: 同步外刊到Cloudflare R2
on:
# 在同步上游仓库工作流完成后运行
workflow_run:
workflows: ["同步上游仓库"]
types:
- completed
# 允许手动触发
workflow_dispatch:
jobs:
sync-to-r2:
runs-on: ubuntu-latest
# 只有当触发的工作流成功完成时才运行
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
# 检出当前仓库代码(使用稀疏检出策略,只检出必要的目录结构)
- name: 检出当前仓库
uses: actions/checkout@v3
with:
fetch-depth: 1
sparse-checkout: |
01_economist
02_new_yorker
04_atlantic
05_wired
sparse-checkout-cone-mode: true
# 安装AWS CLI
- name: 安装AWS CLI
uses: unfor19/install-aws-cli-action@v1
with:
version: 2.22.35
# 配置AWS凭证(用于访问Cloudflare R2)
- name: 配置AWS凭证
run: |
mkdir -p ~/.aws
cat > ~/.aws/credentials << EOF
[default]
aws_access_key_id = ${{ secrets.R2_ACCESS_KEY_ID }}
aws_secret_access_key = ${{ secrets.R2_SECRET_ACCESS_KEY }}
EOF
cat > ~/.aws/config << EOF
[default]
region = auto
output = json
EOF
# 验证配置是否生效
echo "AWS CLI配置完成,当前配置:"
aws configure list
# 列出目录内容,以便于调试
- name: 列出目录内容
run: |
echo "列出经济学人目录内容:"
ls -la ./01_economist || echo "经济学人目录不存在"
echo "列出纽约客目录内容:"
ls -la ./02_new_yorker || echo "纽约客目录不存在"
echo "列出Atlantic目录内容:"
ls -la ./04_atlantic || echo "Atlantic目录不存在"
echo "列出Wired目录内容:"
ls -la ./05_wired || echo "Wired目录不存在"
# 查找最新的经济学人文件
- name: 查找最新的经济学人文件
id: economist
run: |
if [ -d "./01_economist" ]; then
LATEST_ECONOMIST_DIR=$(find ./01_economist -maxdepth 1 -type d -name "te_*" | sort -r | head -n 1)
echo "latest_dir=$LATEST_ECONOMIST_DIR" >> $GITHUB_OUTPUT
# 从目录名中提取日期(格式如te_2025.04.05)
if [ -n "$LATEST_ECONOMIST_DIR" ]; then
DIR_NAME=$(basename "$LATEST_ECONOMIST_DIR")
ECONOMIST_DATE=$(echo "$DIR_NAME" | sed 's/te_//')
echo "date=$ECONOMIST_DATE" >> $GITHUB_OUTPUT
echo "找到最新的经济学人目录: $LATEST_ECONOMIST_DIR,日期: $ECONOMIST_DATE"
fi
else
echo "经济学人目录不存在"
echo "latest_dir=" >> $GITHUB_OUTPUT
fi
# 查找最新的纽约客文件
- name: 查找最新的纽约客文件
id: newyorker
run: |
if [ -d "./02_new_yorker" ]; then
LATEST_NEWYORKER_DIR=$(find ./02_new_yorker -maxdepth 1 -type d -mindepth 1 -not -path "*/\.*" | sort -r | head -n 1)
echo "latest_dir=$LATEST_NEWYORKER_DIR" >> $GITHUB_OUTPUT
# 从目录名中提取日期
if [ -n "$LATEST_NEWYORKER_DIR" ]; then
DIR_NAME=$(basename "$LATEST_NEWYORKER_DIR")
echo "date=$DIR_NAME" >> $GITHUB_OUTPUT
echo "找到最新的纽约客目录: $LATEST_NEWYORKER_DIR,日期: $DIR_NAME"
fi
else
echo "纽约客目录不存在"
echo "latest_dir=" >> $GITHUB_OUTPUT
fi
# 同步经济学人到R2
- name: 同步经济学人到R2
if: steps.economist.outputs.latest_dir != ''
run: |
echo "开始同步经济学人目录: ${{ steps.economist.outputs.latest_dir }}"
if [ -d "${{ steps.economist.outputs.latest_dir }}" ]; then
# 只上传PDF文件
for file in "${{ steps.economist.outputs.latest_dir }}"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/经济学人/${{ steps.economist.outputs.date }}/$filename"
# 检查文件是否已存在
echo "检查R2中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.R2_BUCKET_NAME }} --key "$s3_key" --endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 禁用分块上传,使用单一请求上传文件
aws configure set s3.multipart_threshold 999GB
aws s3 cp "$file" "s3://${{ secrets.R2_BUCKET_NAME }}/$s3_key" \
--endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
--no-progress \
--checksum-algorithm CRC32
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步经济学人PDF到R2"
else
echo "经济学人目录不存在或为空: ${{ steps.economist.outputs.latest_dir }}"
fi
# 同步纽约客到R2
- name: 同步纽约客到R2
if: steps.newyorker.outputs.latest_dir != ''
run: |
if [ -d "${{ steps.newyorker.outputs.latest_dir }}" ]; then
# 只上传PDF文件
for file in "${{ steps.newyorker.outputs.latest_dir }}"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/纽约客/${{ steps.newyorker.outputs.date }}/$filename"
# 检查文件是否已存在
echo "检查R2中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.R2_BUCKET_NAME }} --key "$s3_key" --endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 禁用分块上传,使用单一请求上传文件
aws configure set s3.multipart_threshold 999GB
aws s3 cp "$file" "s3://${{ secrets.R2_BUCKET_NAME }}/$s3_key" \
--endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
--no-progress \
--checksum-algorithm CRC32
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步纽约客PDF到R2"
else
echo "纽约客目录不存在或为空: ${{ steps.newyorker.outputs.latest_dir }}"
fi
# 同步The Atlantic到R2(如果有更新)
- name: 同步The Atlantic到R2
run: |
if [ -d "./04_atlantic" ]; then
LATEST_ATLANTIC_DIR=$(find ./04_atlantic -maxdepth 1 -type d -mindepth 1 -not -path "*/\.*" | sort -r | head -n 1)
if [ -n "$LATEST_ATLANTIC_DIR" ] && [ -d "$LATEST_ATLANTIC_DIR" ]; then
# 从目录名中提取日期
DIR_NAME=$(basename "$LATEST_ATLANTIC_DIR")
ATLANTIC_DATE=$DIR_NAME
echo "找到最新的大西洋月刊目录: $LATEST_ATLANTIC_DIR,日期: $ATLANTIC_DATE"
# 只上传PDF文件
for file in "$LATEST_ATLANTIC_DIR"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/大西洋月刊/$ATLANTIC_DATE/$filename"
# 检查文件是否已存在
echo "检查R2中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.R2_BUCKET_NAME }} --key "$s3_key" --endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 禁用分块上传,使用单一请求上传文件
aws configure set s3.multipart_threshold 999GB
aws s3 cp "$file" "s3://${{ secrets.R2_BUCKET_NAME }}/$s3_key" \
--endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
--no-progress \
--checksum-algorithm CRC32
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步The Atlantic PDF到R2"
else
echo "未找到有效的The Atlantic目录"
fi
else
echo "Atlantic目录不存在"
fi
# 同步Wired到R2(如果有更新)
- name: 同步Wired到R2
run: |
if [ -d "./05_wired" ]; then
LATEST_WIRED_DIR=$(find ./05_wired -maxdepth 1 -type d -mindepth 1 -not -path "*/\.*" | sort -r | head -n 1)
if [ -n "$LATEST_WIRED_DIR" ] && [ -d "$LATEST_WIRED_DIR" ]; then
# 从目录名中提取日期
DIR_NAME=$(basename "$LATEST_WIRED_DIR")
WIRED_DATE=$DIR_NAME
echo "找到最新的连线杂志目录: $LATEST_WIRED_DIR,日期: $WIRED_DATE"
# 只上传PDF文件
for file in "$LATEST_WIRED_DIR"/*.pdf; do
if [ -f "$file" ]; then
# 提取文件名
filename=$(basename "$file")
# 构建S3路径
s3_key="2.外刊/连线杂志/$WIRED_DATE/$filename"
# 检查文件是否已存在
echo "检查R2中是否已存在文件: $s3_key"
if aws s3api head-object --bucket ${{ secrets.R2_BUCKET_NAME }} --key "$s3_key" --endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" 2>/dev/null; then
echo "文件已存在,跳过上传: $s3_key"
else
echo "正在上传PDF文件: $file"
# 禁用分块上传,使用单一请求上传文件
aws configure set s3.multipart_threshold 999GB
aws s3 cp "$file" "s3://${{ secrets.R2_BUCKET_NAME }}/$s3_key" \
--endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
--no-progress \
--checksum-algorithm CRC32
echo "文件上传完成: $s3_key"
fi
fi
done
echo "已同步Wired PDF到R2"
else
echo "未找到有效的Wired目录"
fi
else
echo "Wired目录不存在"
fi
四、最终效果
可以看到执行同步后,对应刊物的最新文章就被我们同步到Obsidian的Vault里了。
五、可能的改进优化
1.更多的同步平台
我们目前的解决方法是利用 GitHub Actions 将文件推送到 S3 兼容的云存储(如腾讯云 COS),然后依赖 Obsidian 的 Remotely Save
插件进行同步。但每个人的同步习惯和工具链可能不同:
OneDrive / Google Drive / Dropbox:
- Obsidian 端:
Remotely Save
插件本身就支持 OneDrive、Dropbox 等多种后端。如果你已经在 Obsidian 中配置了这些服务的同步,那么 Obsidian 这边不需要改动。 - GitHub Actions 端 (挑战): 关键在于修改
sync-to-obsidian.yml
这个 Workflow。你需要找到或创建一个 GitHub Action,该 Action 能够安全地验证并上传文件到你选择的云盘服务。这通常涉及到:- 获取对应云盘服务的 API 访问令牌或使用特定的 CLI 工具 (如
rclone
,一个强大的多云存储管理工具,或者针对特定服务的官方/社区 CLI)。 - 将认证信息(如 API Token)安全地存储在 GitHub Secrets 中。
- 修改 Workflow 中的上传步骤,使用相应的命令将文件推送到你的云盘指定目录。例如,使用
rclone copy
命令。
- 获取对应云盘服务的 API 访问令牌或使用特定的 CLI 工具 (如
- 优点: 可以直接利用你已有的、可能容量更大或更习惯的云盘服务。
- 缺点: 需要额外配置 Actions 与特定云盘服务的集成,可能比 S3 稍微复杂一些。
- Obsidian 端:
Git (例如,使用 Obsidian Git 插件):
- 思路: 不再需要 S3 作为中间存储。GitHub Actions 直接将下载到的最新外刊 PDF 文件提交 (commit) 并推送 (push) 到一个专门用于同步 Obsidian Vault 的 Git 仓库(或者就是你的 Vault 本身如果它是一个 Git 仓库)。
- Obsidian 端: 你需要在 Obsidian 中安装并配置
Obsidian Git
插件,让它定期自动pull
这个 Git 仓库的更新。 - GitHub Actions 端: 修改
sync-to-obsidian.yml
Workflow:- 移除所有 AWS CLI 和 S3 相关的配置和上传步骤。
- 在检出
awesome-english-ebooks
内容后,将找到的最新 PDF 文件复制到 另一个 Git 仓库(你的 Obsidian Vault 仓库)的工作目录中,或者直接复制到当前仓库的某个指定目录下(如果打算用同一个仓库混合管理)。 - 添加 Git 命令步骤 (
git add
,git commit
,git push
) 来提交这些新文件。需要配置好推送权限,可能需要使用 Personal Access Token (PAT) 或 SSH 密钥,并存储在 Secrets 中。
- 优点: 完全基于 Git,对于熟悉 Git 的用户来说可能更自然;减少了对 S3 的依赖。
- 缺点:
- 仓库大小: Git 不擅长处理大型二进制文件。频繁添加 PDF 可能导致 Git 仓库迅速膨胀,克隆和拉取变慢。可能需要结合使用 Git LFS (Large File Storage) 来管理这些 PDF 文件,但这会增加设置的复杂性。
- 冲突处理: 如果你在本地也修改了 Vault,自动
pull
可能会遇到合并冲突,需要手动解决。
2.AI加成下的文件处理
- 我们还可以在 GitHub Actions 中集成 pandoc 等工具(或者使用类似于Mistral OCR一类的工具),尝试将 EPUB 或甚至 PDF 转换为 Markdown 格式。这样可以直接在 Obsidian 中编辑、链接和引用文本内容,而不是仅仅处理一个附件。但这非常复杂,因为格式转换(尤其是 PDF)往往会丢失布局和图片,效果可能不理想,需要大量调整和测试。
- 可以尝试在 Action 中解析文件名或文件内容(如果可能),提取期刊名称、期号、日期等元数据。然后,除了上传原始文件,还可以自动生成一个对应的 Markdown 笔记文件(.md),包含这些元数据、指向原始 PDF/EPUB 文件的链接以及可能的标签(如 #外刊 #经济学人 #待读)。这将极大地提升在 Obsidian 中的组织和可发现性。这可能需要编写更复杂的脚本(如 Python 脚本)并在 Action 中运行。
说实话我觉得这样不如自己notion开一个database,确实没必要自己硬在Obsidian里实现一个徒增维护成本的数据库