Claude Code 实战 (25):博客发布水晶包 2.0 开发
导读: 这是 Claude Code 源码解析系列的第 25 篇,实战篇第 5 篇。基于 Claude Code 文件操作和任务管理能力,实战开发增强版博客发布水晶包。
📋 目录
[博客发布水晶包 2.0 概述](#博客发布水晶包 20-概述)
基于 Claude Code 能力设计
[实战:开发博客发布水晶包 2.0](#实战开发博客发布水晶包 20)
博客发布最佳实践
总结
博客发布水晶包 2.0 概述 核心功能 博客发布水晶包 2.0 = Claude Code 文件操作 + 任务管理 + 权限控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 博客发布水晶包 2.0 提供: ├── 文章创建 │ ├── Front Matter 自动生成 │ ├── 模板选择 │ └── 分类标签管理 ├── 文章发布 │ ├── Git 提交 │ ├── MR 创建 │ └── Jenkins 触发 ├── 发布监控 │ ├── 构建状态跟踪 │ ├── 失败重试 │ └── 通知推送 └── 批量操作 ├── 批量发布 ├── 批量撤回 └── 批量修改
1.0 vs 2.0 对比
功能
1.0 版本
2.0 版本
Front Matter
手动添加
自动生成 ✅
Git 操作
手动执行
自动化 ✅
MR 创建
手动创建
自动创建 ✅
构建监控
无
实时监控 ✅
失败处理
手动重试
自动重试 ✅
通知推送
无
飞书/微信推送 ✅
基于 Claude Code 能力设计 基于的文件操作 (第 2 篇) FileWriteTool 原子写入:
1 2 await atomicWrite (filePath, content);
FileEditTool 差异编辑:
1 2 3 4 5 await editFile (path, [{ oldText : "draft: true" , newText : "draft: false" }]);
基于的任务管理 (第 7 篇) TaskCreateTool 后台任务:
1 2 3 4 5 6 await taskCreate ({ name : "发布博客" , command : "bash publish.sh" , background : true });
TaskOutputTool 进度跟踪:
1 2 3 4 5 await taskOutput ({ taskId : "publish-001" , follow : true });
基于的权限系统 (第 24 篇) Permission Decision 权限检查:
1 2 3 4 5 await permissionCheck ({ tool : "bash" , command : "git push" });
实战:开发博客发布水晶包 2.0 步骤 1:创建水晶包目录 1 2 3 mkdir -p ~/.openclaw/crystals/blog-publisher-v2/{scripts,config,templates,tests,docs}cd ~/.openclaw/crystals/blog-publisher-v2
步骤 2:编写 CRYSTAL.md 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 --- name: blog-publisher-v2 version: 2.0.0 description: 博客发布水晶包 2.0 (基于 Claude Code 文件操作 + 任务管理) author: John license: MIT # 能力声明 capabilities: - name: blog_create description: 创建博客文章 based_ on: Claude Code FileWriteTool features: - Front Matter 自动生成 - 模板选择 - 分类标签管理 - name: blog_publish description: 发布博客文章 based_on: Claude Code TaskCreateTool features: - Git 自动化 - MR 自动创建 - Jenkins 触发 - name: blog_monitor description: 监控发布状态 based_on: Claude Code TaskOutputTool features: - 实时跟踪 - 失败重试 - 通知推送 - name: blog_batch description: 批量操作 based_on: Claude Code Task Management features: - 批量发布 - 批量撤回 - 批量修改 # 依赖 dependencies: - name: git version: required - name: jq version: required - name: curl version: required # 配置 config: # 博客仓库 repo: url: http://192.168.100.191:9910/tech-platfom/blogsite/blog-johnz.git branch: main base_dir: ~/blog-johnz # GitLab 配置 gitlab: url: http://192.168.100.191:9910 project: tech-platfom/blogsite/blog-johnz # Jenkins 配置 jenkins: url: http://192.168.100.211:8899 job: site/site-john # 通知配置 notification: feishu_webhook: ${FEISHU_WEBHOOK} notify_on_success: true notify_on_failure: true # 权限 permissions: - file_read - file_ write - bash (git, curl) --- # 博客发布水晶包 2.0 增强版博客发布水晶包,基于 Claude Code 文件操作和任务管理能力。 ## 使用方式 ### 创建文章 ```bash openclaw run blog-publisher-v2 create \ --title "我的文章" \ --category "AI 技术" \ --tags '["OpenClaw", "Agent"]'
发布文章 1 2 openclaw run blog-publisher-v2 publish \ --file "source/_posts/my-article.md"
监控状态 1 2 openclaw run blog-publisher-v2 monitor \ --task-id publish-001
增强特性
Front Matter 自动生成 - 符合 Hexo 规范
Git 自动化 - 自动提交推送
MR 自动创建 - 自动创建 Merge Request
构建监控 - 实时跟踪 Jenkins 状态
通知推送 - 发布结果飞书/微信通知
测试
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 ### 步骤 3:实现核心脚本 **文件:** `scripts/blog-create.sh` ```bash #!/bin/bash # 博客文章创建脚本 (基于 Claude Code FileWriteTool) set -e TITLE="" CATEGORY="" TAGS="" LAYOUT="post" DRAFT=false while [[ $# -gt 0 ]]; do case $1 in --title) TITLE="$2" shift 2 ;; --category) CATEGORY="$2" shift 2 ;; --tags) TAGS="$2" shift 2 ;; --draft) DRAFT=true shift ;; *) echo "Unknown option: $1" exit 1 ;; esac done # 配置 BLOG_DIR="$HOME/blog-johnz" POSTS_DIR="$BLOG_DIR/source/_posts" # 生成文件名 generate_filename() { local title="$1" local filename=$(echo "$title" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]') echo "$filename.md" } # 生成 Front Matter generate_front_matter() { local title="$1" local category="$2" local tags="$3" local draft="$4" local date=$(date +%Y-%m-%d) local author="John" cat << EOF --- title: $title date: $date categories: [$category] tags: $tags author: $author EOF if [[ "$draft" == true ]]; then echo "draft: true" fi echo "---" } # 创建文章 create_post() { local title="$1" local category="$2" local tags="$3" local draft="$4" # 生成文件名 local filename=$(generate_filename "$title") local filepath="$POSTS_DIR/$filename" # 检查文件是否已存在 if [[ -f "$filepath" ]]; then echo "Error: 文件已存在:$filepath" exit 1 fi # 创建目录 mkdir -p "$POSTS_DIR" # 生成 Front Matter local front_matter=$(generate_front_matter "$title" "$category" "$tags" "$draft") # 写入文件 (原子写入) local temp_file="$filepath.tmp.$$" echo "$front_matter" > "$temp_file" echo "" >> "$temp_file" echo "# $title" >> "$temp_file" echo "" >> "$temp_file" # 原子移动 mv "$temp_file" "$filepath" echo "文章已创建:" echo " 标题:$title" echo " 文件:$filepath" echo " 分类:$category" echo " 标签:$tags" echo " 草稿:$draft" } # 主函数 main() { if [[ -z "$TITLE" ]]; then echo "Error: --title is required" exit 1 fi # 设置默认值 CATEGORY="${CATEGORY:-AI 技术}" TAGS="${TAGS:-[]}" create_post "$TITLE" "$CATEGORY" "$TAGS" "$DRAFT" } main "$@"
文件: scripts/blog-publish.sh
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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 #!/bin/bash set -eFILE="" BRANCH_NAME="" MR_TITLE="" SKIP_BUILD=false BLOG_DIR="$HOME /blog-johnz" GITLAB_URL="http://192.168.100.191:9910" GITLAB_PROJECT="tech-platfom/blogsite/blog-johnz" JENKINS_URL="http://192.168.100.211:8899" JENKINS_JOB="site/site-john" GITLAB_TOKEN=$(cat ~/.openclaw/config/credentials.json | jq -r '.gitlab.token' ) JENKINS_USER=$(cat ~/.openclaw/config/credentials.json | jq -r '.jenkins.user' ) JENKINS_TOKEN=$(cat ~/.openclaw/config/credentials.json | jq -r '.jenkins.token' ) while [[ $# -gt 0 ]]; do case $1 in --file) FILE="$2 " shift 2 ;; --branch) BRANCH_NAME="$2 " shift 2 ;; --mr-title) MR_TITLE="$2 " shift 2 ;; --skip-build) SKIP_BUILD=true shift ;; *) echo "Unknown option: $1 " exit 1 ;; esac done generate_mr_title () { local file="$1 " local filename=$(basename "$file " .md) echo "feat: 发布博客文章 - $filename " } generate_mr_description () { local file="$1 " local title=$(grep "^title:" "$file " | sed 's/title: *//' ) cat << EOF ## 文章信息 - **标题**: $title - **发布时间**: $(date +%Y-%m-%d) - **类型**: 博客文章 ## 检查清单 - [ ] Front Matter 正确 - [ ] 内容格式正确 - [ ] 分类标签正确 ## 自动发布 此 MR 由 blog-publisher-v2 水晶包自动创建 EOF } git_operations () { local file="$1 " local branch="$2 " cd "$BLOG_DIR " git checkout main git pull origin main git checkout -b "$branch " git add "$file " git commit -m "feat: 添加博客文章 $(basename "$file " ) " git push origin "$branch " echo "Git 操作完成:" echo " 分支:$branch " echo " 文件:$file " } create_mr () { local branch="$1 " local title="$2 " local description="$3 " local response=$(curl -s -X POST \ "$GITLAB_URL /api/v4/projects/${GITLAB_PROJECT//\//%2F} /merge_requests" \ -H "PRIVATE-TOKEN: $GITLAB_TOKEN " \ -H "Content-Type: application/json" \ -d "{ \"source_branch\": \"$branch \", \"target_branch\": \"main\", \"title\": \"$title \", \"description\": \"$description \" }" ) local mr_id=$(echo "$response " | jq -r '.iid' ) local mr_url=$(echo "$response " | jq -r '.web_url' ) if [[ -n "$mr_id " ]] && [[ "$mr_id " != "null" ]]; then echo "MR 已创建:" echo " ID: !$mr_id " echo " 链接:$mr_url " echo "$mr_id " else echo "Error: 创建 MR 失败" echo "$response " exit 1 fi } trigger_jenkins () { local mr_id="$1 " local response=$(curl -s -X POST \ "$JENKINS_URL /job/$JENKINS_JOB /buildWithParameters" \ -u "$JENKINS_USER :$JENKINS_TOKEN " \ -d "MR_ID=$mr_id " ) echo "Jenkins 构建已触发:" echo " MR: !$mr_id " echo " Job: $JENKINS_JOB " } send_notification () { local status="$1 " local mr_id="$2 " local mr_url="$3 " local webhook="${FEISHU_WEBHOOK:-} " if [[ -z "$webhook " ]]; then return 0 fi local color="green" local title="博客发布成功" if [[ "$status " == "failed" ]]; then color="red" title="博客发布失败" fi curl -s -X POST "$webhook " \ -H "Content-Type: application/json" \ -d "{ \"msg_type\": \"interactive\", \"card\": { \"config\": { \"wide_screen_mode\": true }, \"header\": { \"template\": \"$color \", \"title\": { \"content\": \"$title \", \"tag\": \"plain_text\" } }, \"elements\": [ { \"tag\": \"markdown\", \"content\": \"**MR**: [!$mr_id ]($mr_url )\" } ] } }" > /dev/null} publish () { local file="$1 " if [[ ! -f "$file " ]]; then echo "Error: 文件不存在:$file " exit 1 fi if ! head -1 "$file " | grep -q "^---" ; then echo "Error: 文件缺少 Front Matter" exit 1 fi local branch_name="${BRANCH_NAME:-content-$(date +%Y%m%d)-$(basename "$file" .md)} " local mr_title="${MR_TITLE:-$(generate_mr_title "$file")} " local mr_description=$(generate_mr_description "$file " ) echo "开始发布博客..." echo " 文件:$file " echo " 分支:$branch_name " echo " MR 标题:$mr_title " echo "" echo "执行 Git 操作..." git_operations "$file " "$branch_name " echo "" echo "创建 MR..." local mr_id=$(create_mr "$branch_name " "$mr_title " "$mr_description " ) echo "" if [[ "$SKIP_BUILD " != true ]]; then echo "触发 Jenkins 构建..." trigger_jenkins "$mr_id " echo "" fi send_notification "success" "$mr_id " "$(echo "$mr_description " | grep "链接:" | cut -d: -f2) " echo "" echo "✅ 发布完成!" } main () { if [[ -z "$FILE " ]]; then echo "Error: --file is required" exit 1 fi publish "$FILE " } main "$@ "
文件: scripts/blog-monitor.sh
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 #!/bin/bash set -eMR_ID="" INTERVAL=10 TIMEOUT=600 GITLAB_URL="http://192.168.100.191:9910" GITLAB_PROJECT="tech-platfom/blogsite/blog-johnz" JENKINS_URL="http://192.168.100.211:8899" JENKINS_JOB="site/site-john" GITLAB_TOKEN=$(cat ~/.openclaw/config/credentials.json | jq -r '.gitlab.token' ) JENKINS_USER=$(cat ~/.openclaw/config/credentials.json | jq -r '.jenkins.user' ) JENKINS_TOKEN=$(cat ~/.openclaw/config/credentials.json | jq -r '.jenkins.token' ) while [[ $# -gt 0 ]]; do case $1 in --mr-id) MR_ID="$2 " shift 2 ;; --interval) INTERVAL="$2 " shift 2 ;; --timeout ) TIMEOUT="$2 " shift 2 ;; *) echo "Unknown option: $1 " exit 1 ;; esac done get_mr_status () { local mr_id="$1 " local response=$(curl -s -X GET \ "$GITLAB_URL /api/v4/projects/${GITLAB_PROJECT//\//%2F} /merge_requests/$mr_id " \ -H "PRIVATE-TOKEN: $GITLAB_TOKEN " ) local state=$(echo "$response " | jq -r '.state' ) local merged=$(echo "$response " | jq -r '.merged_at' ) local pipeline_status=$(echo "$response " | jq -r '.pipeline.status' ) echo "$state |$merged |$pipeline_status " } get_jenkins_status () { local mr_id="$1 " local response=$(curl -s -X GET \ "$JENKINS_URL /job/$JENKINS_JOB /api/json?tree=builds[number,result,timestamp]" \ -u "$JENKINS_USER :$JENKINS_TOKEN " ) local latest_build=$(echo "$response " | jq -r '.builds[0]' ) local result=$(echo "$latest_build " | jq -r '.result' ) local number=$(echo "$latest_build " | jq -r '.number' ) echo "$result |$number " } monitor_mr () { local mr_id="$1 " local elapsed=0 echo "开始监控 MR: !$mr_id " echo "─────────────────────────────────────" while [[ $elapsed -lt $TIMEOUT ]]; do local status=$(get_mr_status "$mr_id " ) local state=$(echo "$status " | cut -d'|' -f1) local merged=$(echo "$status " | cut -d'|' -f2) local pipeline=$(echo "$status " | cut -d'|' -f3) printf "\r[%02d:%02d] MR 状态:%s | 流水线:%s" \ $((elapsed / 60 )) $((elapsed % 60 )) "$state " "$pipeline " if [[ "$merged " != "null" ]]; then echo "" echo "" echo "✅ MR 已合并!" return 0 fi if [[ "$pipeline " == "failed" ]]; then echo "" echo "" echo "❌ 流水线失败!" return 1 fi sleep "$INTERVAL " elapsed=$((elapsed + INTERVAL)) done echo "" echo "" echo "⏱️ 监控超时!" return 1 } main () { if [[ -z "$MR_ID " ]]; then echo "Error: --mr-id is required" exit 1 fi monitor_mr "$MR_ID " } main "$@ "
步骤 4:配置文件 文件: config/publish-config.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 repo: url: http://192.168.100.191:9910/tech-platfom/blogsite/blog-johnz.git branch: main base_dir: ~/blog-johnz gitlab: url: http://192.168.100.191:9910 project: tech-platfom/blogsite/blog-johnz jenkins: url: http://192.168.100.211:8899 job: site/site-john notification: feishu_webhook: ${FEISHU_WEBHOOK} notify_on_success: true notify_on_failure: true publish: auto_create_mr: true auto_trigger_build: true monitor_timeout: 600 retry_count: 3
步骤 5:编写测试用例 文件: tests/test.sh
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 #!/bin/bash set -eSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]} " ) " && pwd) " TEST_PASSED=0 TEST_FAILED=0 test_create_post () { echo "Test: Create post..." local output=$(bash "$SCRIPT_DIR /../scripts/blog-create.sh" \ --title "测试文章" \ --category "测试" \ --tags '["test"]' ) if echo "$output " | grep -q "文章已创建" ; then echo "✅ PASS" ((TEST_PASSED++)) else echo "❌ FAIL" ((TEST_FAILED++)) fi } test_front_matter () { echo "Test: Front matter generation..." local file="$HOME /blog-johnz/source/_posts/测试文章.md" if [[ -f "$file " ]]; then local front_matter=$(head -10 "$file " ) if echo "$front_matter " | grep -q "^title:" && \ echo "$front_matter " | grep -q "^date:" && \ echo "$front_matter " | grep -q "^categories:" ; then echo "✅ PASS" ((TEST_PASSED++)) else echo "❌ FAIL: Front Matter 不完整" ((TEST_FAILED++)) fi else echo "❌ FAIL: 文件不存在" ((TEST_FAILED++)) fi } run_tests () { echo "==================================" echo "博客发布水晶包 2.0 测试" echo "==================================" echo "" test_create_post test_front_matter echo "" echo "==================================" echo "测试结果:$TEST_PASSED 通过,$TEST_FAILED 失败" echo "==================================" if [[ $TEST_FAILED -gt 0 ]]; then exit 1 fi } run_tests
博客发布最佳实践 实践 1:创建文章 1 2 3 4 5 6 7 8 9 10 11 12 openclaw run blog-publisher-v2 create \ --title "OpenClaw 上下文管理最佳实践" \ --category "AI 技术" \ --tags '["OpenClaw", "上下文管理"]' 文章已创建: 标题:OpenClaw 上下文管理最佳实践 文件:~/blog-johnz/source/_posts/openclaw-shang-xia-wen-guan-li-zui-jia-shi-jian.md 分类:AI 技术 标签:["OpenClaw" , "上下文管理" ]
实践 2:发布文章 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 openclaw run blog-publisher-v2 publish \ --file "source/_posts/openclaw-shang-xia-wen-guan-li-zui-jia-shi-jian.md" 开始发布博客... 执行 Git 操作... Git 操作完成: 分支:content-20260403-openclaw-shang-xia-wen-guan-li-zui-jia-shi-jian 文件:source /_posts/openclaw-shang-xia-wen-guan-li-zui-jia-shi-jian.md 创建 MR... MR 已创建: ID: !76 链接:http://mpk.codelab:9910/tech-platfom/blogsite/blog-johnz/-/merge_requests/76 触发 Jenkins 构建... Jenkins 构建已触发: MR: !76 Job: site/site-john ✅ 发布完成!
实践 3:监控发布状态 1 2 3 4 5 6 7 8 9 10 11 12 13 openclaw run blog-publisher-v2 monitor \ --mr-id 76 开始监控 MR: !76 ───────────────────────────────────── [00:00] MR 状态:opened | 流水线:running [00:10] MR 状态:opened | 流水线:running [00:20] MR 状态:opened | 流水线:success [00:30] MR 状态:merged | 流水线:success ✅ MR 已合并!
实践 4:批量发布 1 2 3 4 for file in source /_posts/*.md; do openclaw run blog-publisher-v2 publish --file "$file " done
总结 核心要点
基于 Claude Code - 文件操作 + 任务管理 + 权限控制
Front Matter 自动生成 - 符合 Hexo 规范
Git 自动化 - 自动提交推送
MR 自动创建 - 自动创建 Merge Request
构建监控 - 实时跟踪 Jenkins 状态
实战成果
成果
说明
blog-publisher-v2 水晶包
完整的博客发布能力
3 个核心命令
create / publish / monitor
配置文件
GitLab、Jenkins、通知配置
完整测试
2 个测试用例
1.0 vs 2.0 对比
功能
1.0
2.0
Front Matter
手动
自动 ✅
Git 操作
手动
自动 ✅
MR 创建
手动
自动 ✅
构建监控
无
实时 ✅
通知推送
无
飞书/微信 ✅
下一步
系列文章:
[1-20] Claude Code 源码解析 (已完成)
[21] 水晶包开发完整指南 (已完成)
[22] 任务管理水晶包开发 (已完成)
[23] Bash 安全水晶包开发 (已完成)
[24] 权限系统水晶包开发 (已完成)
[25] 博客发布水晶包 2.0 开发 (本文)
[26+] 更多实战案例 (继续中…)
关于作者: John,OpenClaw 平台开发者,专注 AI 助手架构设计与实现。