VSCode + OpenCode + DeepSeek:博客文章全自动开发流水线实战

三个月前我写了一篇《VSCode 中搭建 OpenCode AI 编程助手完整指南》,那篇文章主要讲怎么装、怎么配。装好之后呢?它到底能帮我们省多少事?

这几个月下来,我摸索出一整套从写文章到自动发布的工作流。今天把我博客的完整流水线拆开给大家看——从在 VSCode 里写 Markdown,到 GitHub Actions 自动构建部署,再到自动同步到微信公众号,全链路自动化,一次编写,多平台分发。


一、OpenCode 项目配置:从通用助手到专属博客架构师

OpenCode 默认是通用的编程助手,但真正好用的是它的项目级自定义配置。通过 .opencode.yamlAGENTS.md 两个文件,我们可以让 AI 完全理解项目上下文,变成专属于这个博客的助手。

1.1 模型配置

我的博客用的是 DeepSeek V4 Flash,配置在 .opencode.yaml 中:

1
2
3
4
model: deepseek/deepseek-v4-flash
apiBaseUrl: https://api.deepseek.com
role: Unity导师, 博客架构师, 代码审查员, 资深游戏设计师, 资深策划,
高级软件工程师, 高级美工设计师, UI设计师

选 DeepSeek V4 Flash 的原因很直接:速度快、中文理解好、成本趋近于零。写博客场景不像写生产代码,不需要反复推理,更需要的是快速理解和风格一致的自然语言输出。V4 Flash 在中文技术写作上的表现,明显优于同价位的其他模型。

role 字段定义了助手在项目中的多个角色视角,这直接影响了 OpenCode 的回答风格——当你在 /chat 中问博客相关问题时,它会自动切换到”博客架构师”视角;当问代码逻辑时,切换到”高级软件工程师”视角。

1.2 AGENTS.md:项目知识库

真正让 OpenCode 理解这个博客的,是根目录下的 AGENTS.md 文件。它定义了项目的完整 DNA:

  • 项目概览:博客基于 Hexo + Next 主题,目录结构说明
  • 构建命令hexo clean && hexo ghexo shexo d
  • 代码风格:JavaScript 用 'use strict',Python 用 f-string,Markdown 用 bracket-wrapped tags
  • 文件规范:文章命名 kebab-case,相册目录结构,工具脚本目录
  • 禁止操作:不修改 node_modules/,不提交 public/db.json
  • 发布前检查:本地构建测试 → 用户确认 → 提交部署

有了这份配置,OpenCode 在生成代码或文章时就会自动遵循项目规范,不需要每次重复交代上下文。比如我写 hexo new post "xxx" 后让 OpenCode 帮我补全 front-matter,它自动就知道 tags 要用 bracket-wrapped 格式、categories 要写什么、date 用什么格式。


二、文章开发工作流:从想法到成文

有了配置好的助手,实际的写作流程变成这样:

2.1 创建文章骨架

1
hexo new post "VSCode-OpenCode-DeepSeek-博客文章全自动开发流水线实战"

OpenCode 会自动补全 front-matter,然后我通过 /read 查看当前文章模板,通过 /edit 调整结构大纲。

2.2 AI 辅助写作

写作过程中,我经常用到这几个 OpenCode 的 Tools:

  • /read — 查阅已有的文章,确保风格一致。比如写这篇之前,我 /read 了之前那篇 OpenCode 安装指南,避免内容重复
  • /edit — 精确替换文章内容,不用自己拉滚动条找位置
  • /bash — 直接在 VSCode 里跑 hexo s 预览效果
  • /grep/glob — 快速查找项目中已有的配置文件和代码片段

比如我需要确认某个 GitHub Actions 配置的语法,直接 /read .github/workflows/deploy.yml 就能看到完整配置,不用切到文件管理器去找。

2.3 图片处理

博客的图片主要来自钓鱼相册。每次拍完照片,先用 tools/图片重命名.py 批量处理:

1
python tools/图片重命名工具/图片重命名.py --album img2026 --keywords "花碑水库" --species "鲤鱼"

这个脚本会做三件事:

  1. 图片转 WebP 并压缩
  2. 调用 DeepSeek API 自动生成 alt 文本(根据图片内容描述场景)
  3. 生成相册 HTML 代码片段

生成的 HTML 直接贴进 source/fish/img2026/index.md,然后把新图片推送到 fish-images 仓库。

2.4 多仓库管理

我的博客涉及两个 Git 仓库:

仓库 用途 操作
grj1981/BlogCode 博客源码(文章、主题、配置) git push origin master
grj1981/fish-images 图片 CDN 源 git push origin main

OpenCode 的 AGENTS.md 里定义了禁止自动提交的规则,所以每次都是我先手动 review 变更,确认无误后再让它执行提交操作。


三、自动部署:GitHub Actions 接管构建与发布

以前我的部署流程是:本地 hexo clean && hexo g,确认没问题,然后 hexo d。虽然不太麻烦,但人总有忘记的时候。而且如果在外地没有本地环境,就完全没法更新博客。

自从配了 GitHub Actions,部署这件事基本就从我的待办列表里消失了。

3.1 工作流配置

完整的部署流程写在 .github/workflows/deploy.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
48
49
50
51
52
53
name: Deploy Blog

on:
workflow_dispatch: # 手动触发
push:
branches: [master]
paths: # 只监听源码变更
- 'source/_posts/**'
- 'scripts/**'
- 'themes/next/**'
- '_config.yml'
- 'package.json'

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Build site
env:
TIDB_HOST: ${{ secrets.TIDB_HOST }}
TIDB_PORT: ${{ secrets.TIDB_PORT }}
TIDB_USER: ${{ secrets.TIDB_USER }}
TIDB_PASSWORD: ${{ secrets.TIDB_PASSWORD }}
TIDB_DB: ${{ secrets.TIDB_DB }}
run: npx hexo clean && npx hexo generate

- name: Deploy to grj1981.github.io
run: |
echo "${{ secrets.DEPLOY_KEY_B64 }}" | base64 -d > /tmp/deploy_key
chmod 600 /tmp/deploy_key
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
export GIT_SSH_COMMAND="ssh -i /tmp/deploy_key -o StrictHostKeyChecking=accept-new"
git clone git@github.com:grj1981/grj1981.github.io.git /tmp/gh-pages
rsync -av --delete ./public/ /tmp/gh-pages/ --exclude .git
cd /tmp/gh-pages
git add -A
git diff --cached --quiet --exit-code && echo "No changes to deploy" || (
git commit -m "Site updated: $(date -u '+%Y-%m-%d %H:%M:%S')"
GIT_SSH_COMMAND="ssh -i /tmp/deploy_key -o StrictHostKeyChecking=accept-new" git push origin master
)

几个值得一提的设计:

路径过滤:只监听 source/_posts/scripts/themes/next/ 等目录的变更。如果只是改了 README 或者 tools 目录下的脚本,不会触发部署,省 Action 分钟数。

TiDB 凭据注入:博客的评论区热榜数据在构建时从 TiDB Cloud 读取,通过 GitHub Secrets 注入环境变量,不会暴露在源码中。

SSH Deploy Key:部署到 grj1981.github.io 仓库用的是 base64 编码的 SSH 私钥,存放在 DEPLOY_KEY_B64 Secret 中。构建时解码写入临时文件,用完即弃。

3.2 构建流程时序

1
2
3
4
5
6
7
用户 push → GitHub 检测到变更 →
→ checkout 源码
→ npm ci 安装依赖
→ hexo clean && hexo generate (注入 TiDB 凭据)
→ rsync public/ 到 gh-pages 仓库
→ git commit && git push
→ GitHub Pages 自动更新

整个过程大约 40-60 秒。文章推送到 master 后,不到一分钟就能在 www.bytefisher.top 上看到更新。

3.3 踩坑:路径过滤不生效

🔴 问题:配置了 paths 过滤后,有几次改了 source/_posts/ 下的文章却没触发部署。

排查发现是 GitHub Actions 的 paths 过滤规则的问题——如果 push 包含多个 commit,其中只有部分 commit 命中 paths,整个 workflow 也可能被跳过。最终在配置里加了 workflow_dispatch 手动触发作为兜底,需要时去 GitHub 仓库页面点一下 “Run workflow” 就行。


四、公众号同步:博客到微信的自动化桥梁

部署到博客只是第一步。我的内容还需要同步到微信公众号,而这一步的自动化程度最高,也最复杂。

4.1 同步方案设计

微信公众号文章的格式要求和博客完全不同:

  • 不支持标准 Markdown,需要转成内联 HTML
  • 图片不能引用外链,必须上传到微信素材库
  • 封面图有严格的尺寸要求(900×500)
  • 已经发布的草稿不支持修改(只能重新创建)

面对这些限制,我决定写一个专门的工具,目录在 tools/wechat-sync/。整体架构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
source/_posts/*.md


post-parser.js ─── 解析 Front Matter + 正文


markdown-to-html.js ─── 转换为微信公众号兼容的 HTML


image-processor.js ─── 下载图片 → 压缩 → 上传微信素材库


wechat-api.js ─── 调用微信公众号 API 创建/更新草稿


tracker.js ─── 记录同步状态(增量同步)


sync-state.json ─── 持久化状态文件

4.2 自动同步工作流

同步也是通过 GitHub Actions 触发,配置在 .github/workflows/wechat-sync.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
name: WeChat Sync

on:
push:
branches: [master]
paths:
- 'source/_posts/**'
workflow_dispatch:
inputs:
mode:
description: '同步模式'
default: 'auto'
type: choice
options:
- auto
- all
- year-2026
- dry-run
specific_file:
description: '指定单个文件'
required: false
type: string
force:
description: '强制重新同步'
required: false
type: boolean
default: false

concurrency:
group: wechat-sync
cancel-in-progress: false

工作流的触发方式有两种:

自动触发:当有新的文章 push 到 source/_posts/ 时,自动检测变更的文章,只同步新增或内容有修改的文章。

手动触发:通过 GitHub Actions 的 workflow_dispatch,支持多种模式:

  • auto — 自动检测增量变更(默认)
  • all — 强制同步全部文章
  • year-2026 — 只同步指定年份
  • dry-run — 试运行,不调用微信 API,只输出将要执行的操作
  • specific_file — 指定单个文件同步

4.3 核心模块拆解

4.3.1 文章解析(post-parser.js)

这个模块负责把 Hexo 的 Markdown 文章解析成结构化数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function parsePost(filePath) {
const rawContent = fs.readFileSync(filePath, 'utf-8');

// 解析 Front Matter
const endOfFrontMatter = rawContent.indexOf('---', 3);
let frontMatter = yaml.load(rawContent.slice(3, endOfFrontMatter));

// 提取正文
const body = rawContent.slice(endOfFrontMatter + 3).trim();

// 解析图片链接(支持 ![]() 和 <img> 两种格式)
const imageRegex = /!\[.*?\]\((.*?)\)|<img[^>]+src=["'](.*?)["']/g;
const images = [];
let match;
while ((match = imageRegex.exec(body)) !== null) {
const url = match[1] || match[2];
if (url && !url.startsWith('data:')) images.push(url);
}

return {
title, date, tags, categories,
body, images, permalink, rawContent, ...
};
}

关键点:摘要生成逻辑——优先使用 description 字段,其次是 <!--more--> 之前的内容,最后是正文前 200 字符。

4.3.2 Markdown 转微信 HTML(markdown-to-html.js)

微信公众号的编辑器不支持标准 Markdown,必须使用内联样式的 HTML。我用 marked 库自定义了一个 Renderer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const renderer = new marked.Renderer();

renderer.heading = function ({ text, depth }) {
const tag = `h${Math.min(depth, 4)}`;
const style = STYLES[tag];
return `<${tag} style="${style}">${text}</${tag}>`;
};

renderer.image = function ({ href, text }) {
// 微信不支持外链图片,用占位符标记,后面替换
return `<!-- IMG:${href} -->`;
};

renderer.code = function ({ text, lang }) {
const escaped = escapeHtml(text);
return `<pre style="${STYLES.pre}"><code style="${STYLES.code}">${escaped}</code></pre>`;
};

图片处理是最特别的——它不是直接输出 <img> 标签,而是生成 <!-- IMG:url --> 占位符,等图片上传到微信素材库后,再统一替换成真正的微信图片地址。

安全性方面,内置了 sanitizeHtml 函数过滤 XSS:

1
2
3
4
5
6
7
function sanitizeHtml(html) {
return html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<iframe[\s\S]*?<\/iframe>/gi, '')
.replace(/\son\w+="[^"]*"/gi, '')
.replace(/href=["']javascript:["']/gi, 'href="#"');
}

4.3.3 图片处理流水线(image-processor.js)

图片处理是整个同步过程中最耗时、也最容易出问题的环节。流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function processImages(imageUrls, client, options = {}) {
for (const [index, url] of imageUrls.entries()) {
// 1. 下载图片(带 15s 超时)
const { buffer } = await downloadImage(url, 15000);

// 2. 统一格式(png 太大 → 转 jpg,gif 动图保持)
const { buffer: normalized } = await normalizeImageFormat(buffer);

// 3. 超过 2MB 则压缩(quality 从 85 递减到 40)
if (normalized.length > 2 * 1024 * 1024) {
processedBuffer = await compressImage(normalized, 'jpeg');
}

// 4. 上传到微信素材库
const { url: wechatUrl } = await client.uploadImage(processedBuffer, fileName);
urlMapping.set(url, wechatUrl);
}
}

压缩策略非常保守——先从 quality 85 开始,如果超过 2MB 就降 10,循环到 40 为止。实测绝大多数图片在 quality 75 左右就能压到 2MB 以内。

4.3.4 微信 API 封装(wechat-api.js)

微信公众号 API 有两个关键接口需要调用:

获取 Access Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async getAccessToken() {
if (this._token && Date.now() < this._tokenExpireAt - 5 * 60 * 1000) {
return this._token; // 缓存复用
}

const url = `https://api.weixin.qq.com/cgi-bin/token`
+ `?grant_type=client_credential`
+ `&appid=${this.appId}&secret=${this.appSecret}`;

const data = await (await fetch(url)).json();
this._token = data.access_token;
this._tokenExpireAt = Date.now() + data.expires_in * 1000;
return this._token;
}

创建/更新草稿

1
2
3
4
5
6
7
8
9
10
11
12
13
async createDraft(article) {
const result = await this._request('POST', '/cgi-bin/draft/add', {
articles: [article]
});
return { mediaId: result.media_id };
}

async updateDraft(mediaId, article) {
await this._request('POST', '/cgi-bin/draft/update', {
media_id: mediaId,
articles: article,
});
}

创建草稿时传入的参数包括:标题、作者、摘要、正文 HTML、原文链接、是否开启评论等。其中 content_source_url 字段指向博客原文,读者可以点击”阅读原文”跳转到博客。

4.3.5 增量同步状态追踪(tracker.js)

这是保证同步效率和避免重复创建草稿的关键模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
class SyncTracker {
static computeHash(content) {
return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
}

checkStatus(fileName, contentHash) {
const record = this.state.posts[fileName];
if (!record) return { state: 'unsynced' };
if (record.status === 'failed') return { state: 'failed' };
if (record.contentHash !== contentHash) return { state: 'changed', draftMediaId: record.draftMediaId };
return { state: 'synced', draftMediaId: record.draftMediaId };
}
}

同步状态文件 sync-state.json 记录了每篇文章的同步状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"version": 1,
"updatedAt": "2026-05-31T10:30:00.000Z",
"posts": {
"VSCode-OpenCode-DeepSeek-博客文章全自动开发流水线实战.md": {
"contentHash": "a3f8b2c1...",
"draftMediaId": "abc123def456",
"title": "VSCode + OpenCode + DeepSeek:博客文章全自动开发流水线实战",
"syncedAt": "2026-05-31T10:30:00.000Z",
"status": "created"
}
}
}

增量同步的逻辑:

  1. 对所有文章计算 contentHash
  2. 和状态文件中的记录对比
  3. 新增 → 创建草稿
  4. 内容变更 → 更新草稿
  5. 未变更 → 跳过

这样即使博客有上百篇文章,每次触发时也只需要处理最近变更的那一两篇,整个同步过程能在 10-20 秒内完成。

4.4 踩坑集锦

公众号同步过程中踩过的坑不少,列几个比较典型的:

🔴 踩坑 1:图片尺寸超限

微信素材库对图片的限制是 2MB(普通图片)和 10MB(高清图片),但博客的图片经过 WebP 压缩后很多只有 200-300KB,按理不会超限。问题出在格式转换上——有些 png 图片在转成 jpg 时,由于色彩复杂,文件体积反而膨胀了。

解决方案:compressImage 函数在转格式后主动检测文件大小,超标就循环降 quality,直到达标为止。

🔴 踩坑 2:Markdown 差异导致排版错乱

标准 Markdown 的很多特性在微信公众号里是不支持的:

  • 表格没有带边框 → 需要手动加 border:1px solid #ddd
  • - [ ] 复选框 → 完全不兼容
  • 行内 HTML 标签 → 微信编辑器会过滤掉一部分

解决方案:自建 Renderer,每个元素都指定完整的内联样式,从 h1blockquotetable,全部显式设置 font-sizecolormargin

🔴 踩坑 3:公众号 API 的 media_id 过期

微信的草稿 media_id草稿已发布后会失效,此时再调用 updateDraft 会返回错误码 40007。而且已发布的文章不能修改,必须重新创建。

解决方案:在 syncPost 函数中捕获 40007 错误,自动转为 createDraft 重新创建:

1
2
3
4
5
if (err.errcode === 40007) {
console.log(`⚠️ media_id 无效,尝试重新创建...`);
const result = await client.createDraft(article);
tracker.markSynced(fileName, result.mediaId, ...);
}

🔴 踩坑 4:GitHub Actions 并发冲突

有一次连续 push 了两篇文章,触发了两次 wechat-sync 工作流。两个并行的同步实例同时对 sync-state.json 进行读写,导致状态文件损坏,部分文章重复同步。

解决方案:在 workflow 中设置 concurrency 组:

1
2
3
concurrency:
group: wechat-sync
cancel-in-progress: false

这样同一个 group 内的 workflow 会排队执行,不会并发。cancel-in-progress: false 保证不会取消已经在执行的同步任务。


五、一条指令完成全流程

现在,整个文章开发流程变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
写文章

├── hexo new post "标题" ← OpenCode 自动补全 front-matter
├── 在 VSCode 中写作 ← OpenCode /edit /grep /read
├── 图片处理 ← tools/图片重命名.py


git push origin master

├── GitHub Actions Deploy ← hexo g → rsync → 上线
│ └── www.bytefisher.top 已更新

└── GitHub Actions WeChat Sync ← 解析 → 转 HTML → 传图片 → 创建草稿
└── 公众号草稿箱已就绪,登录手动发布
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
                  ┌─────────────────────┐
│ VSCode + OpenCode │
│ 写文章 / 处理图片 │
└─────────┬───────────┘
│ git push

┌─────────────────────┐
│ GitHub Actions │
│ (自动触发) │
└─────┬───────┬───────┘
│ │
┌────────────┘ └────────────┐
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Deploy Workflow │ │ WeChat Sync Workflow │
│ hexo clean && g │ │ 解析 → 转 HTML │
│ rsync → gh-pages │ │ 压缩图片 → 上传素材 │
│ GitHub Pages 更新 │ │ 创建/更新草稿 │
└─────────────────────┘ └──────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ www.bytefisher.top │ │ 公众号草稿箱 │
│ 已上线 │ │ 手动发布 │
└─────────────────────┘ └──────────────────────┘

这张图就是我现在发布一篇文章的完整路径。从在 VSCode 里敲下最后一个字,到博客上线 + 公众号草稿就绪,只需要 git push 这一条指令。


六、总结

6.1 自动化的价值

这套流水线跑下来,最大的感受是:精力从”发布操作”转移到了”内容创作”

以前每次发文章:

  1. 本地构建 → 检查报错(3 分钟)
  2. 截图 → 上传图床 → 替换链接(5 分钟)
  3. 部署到 GitHub Pages(2 分钟)
  4. 登录公众号后台 → 复制粘贴 → 重新排版(15 分钟)
  5. 上传图片 → 调整格式 → 保存草稿(10 分钟)

现在:

  1. 写完 → git push(10 秒)
  2. 等 40 秒博客自动更新
  3. 等 15 秒公众号草稿自动创建
  4. 登录公众号 → 预览 → 发布(2 分钟)

6.2 成本分析

服务 月费用 说明
DeepSeek V4 Flash API ≈ ¥0 写博客用量极少,免费额度足够
GitHub Actions 免费 每月 2000 分钟免费额度,博客部署只用了不到 50 分钟
GitHub Pages 免费 静态托管免费
Cloudflare Workers 免费 图片代理,免费计划每天 10 万请求
Vercel (Waline) 免费 Serverless 函数运行评论系统
TiDB Cloud Serverless ≈ ¥0 评论区数据存储,25GB 免费容量
总计 ≈ ¥0/月

6.3 未来展望

目前自动化的最后一公里——微信公众号的手动发布——还没有打通。虽然草稿是自动创建好的,但还是需要登录公众号后台点一下发布按钮。

考虑过接入微信发布的 API,但微信公众号的群发接口有严格的调用限制(订阅号每天只能群发 1 次),而且发布后的文章不能修改。自动发布的风险太高,手动预览一次再发布更稳妥。

下一个想接入的是 RSS 自动同步到其他平台(掘金、CSDN 等),通过标准的 RSS feed 让第三方平台自动抓取新文章。atom.xml 已经有了,缺的是各平台的接入适配。


相关链接:


本文对应的完整配置和工具代码可在 GitHub BlogCode 仓库 中找到。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: