编写一个hexo插件 实现将博客中的原创文章同步到微信公众号

前言

这个需求很早以前就有了 由于各种原因耽搁了 这次准备把这个功能搞定

我的大本营在博客上 微信公众号只是附带 不过考虑到微信搜索流量以及公众号的广告收益 所以 还等什么呢 自动化地搞起来 嘿嘿😜

功能实现流程

流程如下:

  1. 在执行hexo d指令是 获取所有文章html
  2. 将内容中带有本文为作者原创的文章过滤出来 并排除wx_pushed.txt中已经同步的文章
  3. 替换文章中的图片
  4. 增加封面图
  5. 替换内链样式
  6. 推送文章到公众号
  7. 推送成功后将文章文件名称记录到wx_pushed.txt中 防止后面重复推送

实现代码

插件代码如下:

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
// hexo-wx-sync.js 自动将hexo博客中的原创文章同步到微信公众号

const axios = require('axios');
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
var req = require('request-promise');
const { marked } = require('marked')
const juice = require('juice')


//1. 获取文章html 将内容中带有'本文由作者原创'的文章过滤出来 排除wx_pushed.txt中已经同步的文章
//2. 替换文章中的图片
//3. 替换内链样式
//4. 增加封面图
//5. 推送文章到公众号
//6. 记录已经推送的文章名称到wx_pushed.txt中

const PLUGIN_NAME = 'hexo-wx-sync';
const config = hexo.config.wx_sync
const parentDir = path.dirname(__dirname);//当前文件的父目录 也就是hexo
const PUSHED_ARTICLES_FILE = `${parentDir}/${PLUGIN_NAME}/wx_pushed.txt`;

let accessToken = ""

let pushedArticles = [];
// 加载已发布的文章列表
function loadPushedArticles() {
if (fs.existsSync(PUSHED_ARTICLES_FILE)) {
pushedArticles = fs.readFileSync(PUSHED_ARTICLES_FILE, 'utf8').split('\n').filter(Boolean);
}
}

// 保存已发布的文章列表
function savePushedArticles() {
fs.writeFileSync(PUSHED_ARTICLES_FILE, pushedArticles.join('\n'));
}





function filterPost(post) {
loadPushedArticles()
// 获取文章路径
const postPath = parentDir + "/source/" + post.source;
// 读取markdown文章内容
const mdContent = fs.readFileSync(postPath, 'utf8');
// 选择还未上传的原创文章
if (!pushedArticles.includes(post.source) && mdContent.includes("本文为作者原创 转载时请注明出处")) {
//将markdown转成html格式
return marked(removeMetaData(mdContent,post))
} else {
return null
}
}

function removeMetaData(content,post) {
// 删除标题
content = content.replace(/^title:.*$/m, "");

// 删除日期
content = content.replace(/^date:.*$/m,`原文地址: ${getPostSourceUrl(post)}<br>如果本文有相关附件下载 请将上面地址复制到浏览器打开` );

// 删除标签
content = content.replace(/^tags:.*$/m, '');

// 删除空行
content = content.replace(/^\s*$/gm, '');



return content;
}

function publishToWechat(parsedContent, post, coverImageId) {
// 发布文章: 'https://api.weixin.qq.com/cgi-bin/material/add_news'

axios.post(`https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${accessToken}`, {
articles: [{
title: `${post.title} `,
thumb_media_id: coverImageId, // 刚才取到的封面图素材 id
author: hexo.config.author,
digest: getDigest(post.content),//文章摘要
content: parsedContent, // 刚才处理好的文章
content_source_url: getPostSourceUrl(post), // 非必填,这里我写的是我博客这篇文章的 URL
need_open_comment: 1, // 是否打开留言功能
show_cover_pic: 0, // 是否把封面图添加到文章开头
}],
}).then(response => {

//推送成功会返回一个media_id
da = response.data
if ('media_id' in da) {
console.log(`${post.title} successfully pushed to WeChat.`);
// 记录已推送的文章名称到 wx_pushed.txt 文件中
// fs.appendFileSync(PUSHED_ARTICLES_FILE, `${post.source}\n`);
pushedArticles.push(post.source);
} else {
console.log(da)
}

}).catch(error => {
console.error(`Failed to push ${post.title} to WeChat: ${error}`);
});;
}
function getDigest(content) {
// 获取文章的摘要信息
const $ = cheerio.load(content);
const summary = $('p').first().text();
return summary
}
function getPostSourceUrl(post) {
// 这里我们使用网站的地址+文件名.html
const domain = hexo.config.url
const urlRegex = /(\d+)[.]md/g;
const match = urlRegex.exec(post.source);
const suffix = match ? match[1] : ''
return domain + "/" + suffix + ".html"
}
// 替换文章中的图片
async function replaceImages(content) {
console.log("开始替换图片")
const $ = cheerio.load(content);
// 获取所有的img元素
const imgs = $('img');
// 使用for循环遍历所有的img元素
for (let i = 0; i < imgs.length; i++) {
const newUrl = await uploadImg($(imgs[i]).attr('src'));
// 修改img标签的src属性
$(imgs[i]).attr('src', newUrl);
}
console.log("图片替换完成")
return $.html();
}


async function request(option) {
return await req(option).then((response) => {
console.log("请求成功: " + response);
return response
}).catch( async (err) => {
console.log("请求错误: " + err);
//如果请求失败 重新请求
return await request(option)
return '{"media_id":"fTLNXU-IBCWKkfWOlCRS17gXwNG5_75-7aBJQalLz1BBBE5UJ43cj3JfxCyYiTN-","url":""}'
});
}



// 上传图片到素材库并获取新的图片地址
async function uploadImg(url) {
console.log("需要上传的图片地址:" + url)
const imageStream = (await axios.get(url, { responseType: 'stream' })).data;
const formData = { media: imageStream };
const res = await request({
url: `https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${accessToken}`,
method: 'POST',
formData,
});
return JSON.parse(res).url;
}

// 将内联样式改为行内样式 否则微信平台不识别
function replaceInternalLinks(content) {
//实现内链样式替换逻辑
const themeStr = fs.readFileSync(`${parentDir}/${PLUGIN_NAME}/html-component/theme6.css`, 'utf-8')
let parsedContent = juice(`
<!--插入主题样式-->
<style>
${themeStr}
</style>
<!--插入marked转换后的html-->
<div class="markdown-body">
${content}
`)
//后面这个div可以不用加 否则会多出一个

return removeWhitespaceExceptCodeBlocks(parsedContent);
}
//移除代码块以外的回车换行符
function removeWhitespaceExceptCodeBlocks(parsedContent) {
const $ = cheerio.load(parsedContent);
//预先存储未替换空格的pre标签
const origin = $('pre')
// 删除回车换行
parsedContent = parsedContent.replace(/[\r\n]+/g, '')
const $2 = cheerio.load(parsedContent);
const after = $2('pre')
for (let i = 0; i < after.length; i++) {
// 进行标签替换
$(after[i]).replaceWith($(origin[i]));
}
return $2.html();
}

function addCoverImage() {
const data = fs.readFileSync(`${parentDir}/${PLUGIN_NAME}/coverJson.json`, 'utf8');
const jsonData = JSON.parse(data);
coverList = jsonData["images"]
//随机获取封面图media_id
const randomIndex = Math.floor(Math.random() * coverList.length);
const randomMediaId = coverList[randomIndex]["media_id"];
return randomMediaId
}
async function getAccessToken() {
const res = await request({
url: `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appid}&secret=${config.secret}`,
method: 'GET',
})
return JSON.parse(res).access_token
}



// 在执行 hexo d 时触发
hexo.extend.generator.register('wechat', async function (locals) {
if (!config.enable) {
console.log('关闭hexo-wx-sync插件');
return;
}
//获取微信公众号accesToken
accessToken = await getAccessToken()
console.log("accessToken" + accessToken)
// 遍历所有文章
locals.posts.forEach(async post => {//post.content 获取的是html格式内容
//过滤出未上传且为原创的文章
const content = filterPost(post)
if (content == null) {
return
}
// 替换文章中的图片
// const replacedContent = await replaceContentImage(content);
const replacedContent = await replaceImages(content);
//将样式注入到标签中
const finalContent = replaceInternalLinks(replacedContent);
// 增加封面图
const coverImageId = addCoverImage();
console.log("finalContent" + finalContent)
// 调用微信接口发布文章
publishToWechat(finalContent, post, coverImageId);
});
});

使用方法:

script目录下新建一个自定义名称.js文件, 注意名称不要使用index.js, 将上面代码粘贴过去, 然后在config配置文件中配置AppIDAppSecret 如下:

1
2
3
4
5
# 将文章同步到微信公众号
wx_sync:
enable: true
appid: 你的appid
secret: 你的secret

关于AppID和AppSecret的获取

开发者ID(AppID)和开发者密码(AppSecret)可以在公众号后台基本配置中获取:

image-20240419133336814

image-20240419133636776

为了确保接口能正常访问, 还需要设置接口请求所在地的ip地址, 如果你在阿里云服务器调用这些接口 则填入服务器的ip , 如果是本地电脑 则在百度搜索IP获取当前公网IP:

image-20240419134212591

image-20240419133544001

相关问题

在进行文章同步过程中 出现很多文章同步不上 接口提示AxiosError: Request failed with status code 501 一般情况下是文章中有些关键字无法通过微信公众号的审核 目前我所知道的关键字有以下几个:

1
2
wget
sql

解决办法也很简单 将这些关键字删除或者拆开 比如wget写成w get这样就能上传成功了 上传完后在公众号后台手动修改回来

虽然有些繁琐 但我暂时还没有想到更好的解决方案 大家如果有好的点子的话 可以 滴滴一下我😜

文章参考 :

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%