
首页

归档

关于

友链
Hexo文章生成工具

Hexo文章生成工具

文章目录

  1. 1. 功能特性
  2. 2. 安装和使用
    1. 2.1. 1. 安装依赖
    2. 2.2. 2. 设置Token
    3. 2.3. 2. 运行工具
  3. 3. 生成的文章格式
  4. 4. 标签和分类处理
    1. 4.1. 示例
  5. 5. 友情链接处理
    1. 5.1. 友情链接页面
    2. 5.2. 友情链接详情
      1. 5.2.1. 支持的格式
      2. 5.2.2. 转换规则
      3. 5.2.3. 生成的YAML格式
  6. 6. 文件命名规则
  7. 7. 配置选项
    1. 7.1. 环境变量配置
    2. 7.2. 代码配置
  8. 8. 注意事项
  9. 9. 输出示例
  10. 10. 故障排除
    1. 10.1. Token错误
    2. 10.2. 网络错误
    3. 10.3. 权限错误
  11. 11. 完整代码
IT小强
IT小强
while(!success) { try(); } // 直到破墙而出
文章
6
分类
3
标签
9

首页

归档

关于

友链
2025-11-30 2025-12-05
代码工具

这个工具用于从 CNB Cool API 获取 issues 数据并自动生成 Hexo 博客文章。

功能特性

  • 🔄 自动分页获取所有 issues
  • 📝 获取 issue 详情(包含 body 内容)
  • 🏷️ 智能解析标签:以”分类:”开头的标签转为分类,其他为普通标签
  • 👤 自动提取作者、时间等信息
  • ⏱️ 内置1秒延迟防止API限制
  • 📁 生成标准 Hexo 文章格式(文件名:post-{number}.md)
  • 🔄 覆盖已存在文件
  • ⚙️ 可配置API地址
  • 🔗 支持友情链接页面和详情处理
  • 📋 解析多个友情链接代码块并自动更新数据文件

安装和使用

1. 安装依赖

首先安装dotenv包(用于加载.env文件):

1
2
cd blog
npm install dotenv

2. 设置Token

方法一:使用.env文件(推荐)

1
2
3
# 在blog目录创建.env文件
cp ../.env.example .env
# 编辑.env文件,填入你的token和API地址

.env文件示例:

1
2
BLOG_CNB_ISSUE_TOKEN=your_token_here
BLOG_CNB_ISSUE_API_URL=https://api.cnb.cool/xqitw/blog/-/issues

方法二:设置环境变量

1
2
export BLOG_CNB_ISSUE_TOKEN="your_token_here"
export BLOG_CNB_ISSUE_API_URL="https://api.cnb.cool/xqitw/blog/-/issues"

方法三:直接修改代码
在 generate-posts.js 中直接修改 CONFIG 对象。

2. 运行工具

在 blog 目录下运行:

1
npm run fetch-posts

或者直接运行:

1
node generate-posts.js

生成的文章格式

每篇文章都会生成标准的 Hexo front matter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
title: Issue标题
date: 2024-01-01T00:00:00.000Z
updated: 2024-01-01T00:00:00.000Z
author: 作者昵称
author_username: 作者用户名
categories: ["个人笔记"] # 如果有"分类:个人笔记"标签
tags: ["普通标签1", "普通标签2"] # 普通标签
comment_count: 0
issue_number: 123
state: open
---

Issue内容...

标签和分类处理

工具会智能处理issue标签:

  • 分类标签:以 分类: 开头的标签 → 转为Hexo分类(categories),自动移除前缀
  • 普通标签:其他所有标签 → 保持为Hexo标签(tags)

示例

如果issue有以下标签:

  • 分类:个人笔记 → 分类:个人笔记
  • 分类:技术分享 → 分类:技术分享
  • JavaScript → 标签:JavaScript
  • 前端 → 标签:前端

生成的front matter:

1
2
categories: ["个人笔记", "技术分享"]
tags: ["JavaScript", "前端"]

友情链接处理

工具支持自动处理友情链接相关的issues:

友情链接页面

当issue标题为”友情链接”时,会生成到 source/friend/index.md,包含标准的Hexo front matter和页面内容。

友情链接详情

当issue标题为”友情链接详情”时,工具会解析issue body中的所有代码块,提取友情链接信息并更新 source/friend/_data.yml 文件。

支持的格式

代码块中的友情链接信息应按以下格式填写:

1
2
3
4
5
6
7
站点名称:小强IT屋

站点地址:https://xqitw.cn

站点描述:IT小强的个人博客,这是一间开源分享的实验室,也是每位技术同路人的交流驿站!

站点头像:https://xqitw.cn/favicon.ico

转换规则

  • 支持中英文冒号(:和:)
  • 自动解析多个代码块
  • 站点名称和站点地址为必需字段
  • 站点描述和站点头像为可选字段
  • 自动去重,相同名称的链接会被更新

生成的YAML格式

1
2
3
4
- name: 小强IT屋
url: https://xqitw.cn
desc: IT小强的个人博客,这是一间开源分享的实验室,也是每位技术同路人的交流驿站!
image: https://xqitw.cn/favicon.ico

文件命名规则

  • 普通文章:post-{issue_number}.md(例如:post-123.md)
  • 关于页面:当issue标题为”关于”时,生成到 about/index.md
  • 友情链接页面:当issue标题为”友情链接”时,生成到 friend/index.md
  • 友情链接详情:当issue标题为”友情链接详情”时,解析body中的代码块并更新 friend/_data.yml

配置选项

环境变量配置

  • BLOG_CNB_ISSUE_TOKEN: API认证token(必需)
  • BLOG_CNB_ISSUE_API_URL: API基础URL

代码配置

在 generate-posts.js 中的 CONFIG 对象可以修改:

  • postsDir: 文章保存目录
  • pageSize: 每页获取的issue数量
  • delay: API调用间隔(毫秒)

注意事项

  1. API限制: 工具内置了1秒延迟防止被API限制
  2. 文件覆盖: 已存在的文章会被自动覆盖
  3. 错误处理: 单个文章获取失败不会影响其他文章
  4. Token安全: 建议使用环境变量而不是硬编码token

输出示例

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
🚀 开始获取issues并生成Hexo文章...
🔗 API地址: https://api.cnb.cool/xqitw/blog/-/issues
📁 文章保存目录: /path/to/blog/source/_posts
🏷️ 分类规则: 以"分类:"开头的标签将作为分类(自动移除前缀)
📄 第 1 页找到 25 个issues
获取issue #123 详情...
🔄 覆盖已存在的文章: post-123.md
获取issue #124 详情...
✅ 已创建文章: post-124.md
获取issue #125 详情...
📄 检测到关于页面,将生成到: about/index.md
✅ 已创建文件: index.md
获取issue #126 详情...
📄 检测到友情链接页面,将生成到: friend/index.md
✅ 已创建文件: index.md
获取issue #127 详情...
✅ 已处理 2 个友情链接
➕ 已添加友情链接: 小强IT屋
🔄 已更新友情链接: CNB
...
✅ 没有更多issues,处理完成

📊 处理完成统计:
✅ 成功处理: 20 篇文章
⏭️ 跳过已存在: 0 篇文章
❌ 处理失败: 0 篇文章
📁 文章保存在: /path/to/blog/source/_posts

故障排除

Token错误

1
❌ 请设置环境变量 BLOG_CNB_ISSUE_TOKEN 或修改CONFIG.token

确保设置了正确的API token。

网络错误

检查网络连接和API地址是否正确。

权限错误

确保文章目录有写入权限。

完整代码

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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
const fs = require('fs');
const path = require('path');
const https = require('https');

// 加载环境变量
function loadEnv() {
try {
const dotenv = require('dotenv');

// 尝试从多个位置加载.env文件
const envPaths = [path.join(__dirname, '.env'), path.join(__dirname, '..', '.env')];

for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
console.log(`📄 加载环境变量文件: ${envPath}`);
dotenv.config({path: envPath});

// 验证token是否加载成功
if (process.env.BLOG_CNB_ISSUE_TOKEN && process.env.BLOG_CNB_ISSUE_TOKEN !== 'YOUR_TOKEN_HERE') {
console.log('✅ Token加载成功');
return true;
} else {
console.log('⚠️ Token未在环境变量中找到');
}
}
}

console.log('⚠️ 未找到.env文件,将直接使用环境变量');
return false;
} catch (e) {
console.log('⚠️ dotenv加载失败:', e.message);
console.log('💡 请运行: npm install dotenv');
return false;
}
}

loadEnv();


// 配置
const CONFIG = {
token: process.env.BLOG_CNB_ISSUE_TOKEN || 'YOUR_TOKEN_HERE', // 从环境变量获取token
baseUrl: process.env.BLOG_CNB_ISSUE_API_URL || 'https://api.cnb.cool/xqitw/blog/-/issues',
postsDir: path.join(__dirname, 'source/_posts'),
pageSize: 100,
delay: 1000 // 1秒延迟
};

// HTTP请求封装
function httpsRequest(url) {
return new Promise((resolve, reject) => {
const options = {
headers: {
'Authorization': `Bearer ${CONFIG.token}`, 'Accept': 'application/json'
}
};

https.get(url, options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
}).on('error', reject);
});
}

// 延迟函数
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// 获取issue列表
async function getIssues(page = 1) {
const url = `${CONFIG.baseUrl}?page=${page}&page_size=${CONFIG.pageSize}&order_by=-created_at`;
console.log(`获取第 ${page} 页issue列表...`);

try {
const issues = await httpsRequest(url);
await sleep(CONFIG.delay);
return issues;
} catch (error) {
console.error(`获取第 ${page} 页失败:`, error.message);
return [];
}
}

// 获取issue详情
async function getIssueDetail(number) {
const url = `${CONFIG.baseUrl}/${number}`;
console.log(`获取issue #${number} 详情...`);

try {
const detail = await httpsRequest(url);
await sleep(CONFIG.delay);
return detail;
} catch (error) {
console.error(`获取issue #${number} 详情失败:`, error.message);
return null;
}
}

// 格式化日期为ISO格式
function formatDate(dateString) {
return new Date(dateString).toISOString();
}

// 解析标签和分类
function parseLabels(labels) {
if (!labels || !Array.isArray(labels)) {
return {categories: [], tags: []};
}

const categories = [];
const tags = [];

labels.forEach(label => {
const labelName = label.name;
if (labelName.startsWith('分类:')) {
// 移除"分类:"前缀,作为分类
categories.push(labelName.substring(3).trim());
} else {
tags.push(labelName);
}
});

return {categories, tags};
}

// 生成Hexo文章front matter
function generateFrontMatter(issue) {
const {categories, tags} = parseLabels(issue.labels);

let frontMatter = `---
title: ${issue.title}
date: ${formatDate(issue.created_at)}
updated: ${formatDate(issue.updated_at)}
author: ${issue.author ? issue.author.nickname : 'Unknown'}
author_username: ${issue.author ? issue.author.username : 'unknown'}
categories: [${categories.map(cat => `"${cat}"`).join(', ')}]
tags: [${tags.map(tag => `"${tag}"`).join(', ')}]
comment_count: ${issue.comment_count || 0}
issue_number: ${issue.number}
state: ${issue.state}
---
`;

return frontMatter;
}

// 解析友情链接详情
function parseFriendLinkDetail(body) {
if (!body) return [];

const friendLinks = [];

// 匹配所有代码块(支持 ```text 和 ``` 等格式)
const codeBlockRegex = /```(?:text|)?\s*([\s\S]*?)```/g;
let match;

while ((match = codeBlockRegex.exec(body)) !== null) {
const codeContent = match[1].trim();
const result = {};

// 使用正则匹配各个字段
const nameMatch = codeContent.match(/站点名称[::]\s*(.+)/);
const urlMatch = codeContent.match(/站点地址[::]\s*(.+)/);
const descMatch = codeContent.match(/站点描述[::]\s*(.+)/);
const imageMatch = codeContent.match(/站点头像[::]\s*(.+)/);

if (nameMatch) result.name = nameMatch[1].trim();
if (urlMatch) result.url = urlMatch[1].trim();
if (descMatch) result.desc = descMatch[1].trim();
if (imageMatch) result.image = imageMatch[1].trim();

// 检查是否所有必要字段都存在
if (result.name && result.url) {
friendLinks.push(result);
}
}

return friendLinks;
}

// 更新友情链接数据文件
async function updateFriendData(friendLinks) {
const dataFilePath = path.join(__dirname, 'source', 'friend', '_data.yml');

try {
let yamlContent = '';
let existingLinks = [];

// 读取现有数据
if (fs.existsSync(dataFilePath)) {
yamlContent = await fs.promises.readFile(dataFilePath, 'utf8');
// 解析现有链接
existingLinks = parseYamlFriendLinks(yamlContent);
}

// 确保目录存在
const friendDir = path.dirname(dataFilePath);
if (!fs.existsSync(friendDir)) {
fs.mkdirSync(friendDir, {recursive: true});
}

// 合并新链接和现有链接
for (const friendLink of friendLinks) {
const existingIndex = existingLinks.findIndex(item => item.name === friendLink.name);
if (existingIndex !== -1) {
existingLinks[existingIndex] = friendLink;
console.log(`🔄 已更新友情链接: ${friendLink.name}`);
} else {
existingLinks.push(friendLink);
console.log(`➕ 已添加友情链接: ${friendLink.name}`);
}
}

// 生成新的YAML内容
const newYamlContent = generateYamlFriendLinks(existingLinks);
await fs.promises.writeFile(dataFilePath, newYamlContent, 'utf8');
return true;
} catch (error) {
console.error(`❌ 更新友情链接数据失败:`, error.message);
return false;
}
}

// 解析YAML格式的友情链接
function parseYamlFriendLinks(yamlContent) {
const links = [];
const lines = yamlContent.split('');
let currentLink = null;

for (const line of lines) {
const trimmedLine = line.trim();

if (trimmedLine.startsWith('- name:')) {
if (currentLink) {
links.push(currentLink);
}
currentLink = {name: trimmedLine.replace('- name:', '').trim()};
} else if (currentLink && trimmedLine.startsWith('url:')) {
currentLink.url = trimmedLine.replace('url:', '').trim();
} else if (currentLink && trimmedLine.startsWith('desc:')) {
currentLink.desc = trimmedLine.replace('desc:', '').trim();
} else if (currentLink && trimmedLine.startsWith('image:')) {
currentLink.image = trimmedLine.replace('image:', '').trim();
}
}

if (currentLink) {
links.push(currentLink);
}

return links;
}

// 生成YAML格式的友情链接
function generateYamlFriendLinks(links) {
if (links.length === 0) return '';

return links.map(link => {
let yaml = `\n- name: ${link.name}`;
if (link.url) yaml += `
url: ${link.url}`;
if (link.desc) yaml += `
desc: ${link.desc}`;
if (link.image) yaml += `
image: ${link.image}`;
return yaml;
}).join('') + '';
}

// 创建文章文件
async function createPostFile(issue) {
let fileName, filePath;

// 判断是否为关于页面
if (issue.title.trim() === '关于') {
fileName = 'index.md';
const aboutDir = path.join(__dirname, 'source', 'about');

// 确保about目录存在
if (!fs.existsSync(aboutDir)) {
fs.mkdirSync(aboutDir, {recursive: true});
console.log(`📁 创建目录: ${aboutDir}`);
}

filePath = path.join(aboutDir, fileName);
console.log(`📄 检测到关于页面,将生成到: about/${fileName}`);
} else if (issue.title.trim() === '友情链接') {
fileName = 'index.md';
const friendDir = path.join(__dirname, 'source', 'friend');

// 确保friend目录存在
if (!fs.existsSync(friendDir)) {
fs.mkdirSync(friendDir, {recursive: true});
console.log(`📁 创建目录: ${friendDir}`);
}

filePath = path.join(friendDir, fileName);
console.log(`📄 检测到友情链接页面,将生成到: friend/${fileName}`);
} else if (issue.title.trim() === '友情链接详情') {
// 处理友情链接详情,解析并更新_data.yml
const friendLinks = parseFriendLinkDetail(issue.body);
if (friendLinks && friendLinks.length > 0) {
await updateFriendData(friendLinks);
console.log(`✅ 已处理 ${friendLinks.length} 个友情链接`);
return true;
} else {
console.log(`⚠️ 无法解析友情链接详情: ${issue.number}`);
return false;
}
} else {
fileName = `post-${issue.number}.md`;
filePath = path.join(CONFIG.postsDir, fileName);
}

const content = generateFrontMatter(issue) + (issue.body || '');

try {
await fs.promises.writeFile(filePath, content, 'utf8');
console.log(`✅ 已创建文件: ${fileName}`);
return true;
} catch (error) {
console.error(`❌ 创建文件失败 ${fileName}:`, error.message);
return false;
}
}

// 检查文件是否已存在
function fileExists(fileName) {
let filePath;
if (fileName === 'about/index.md') {
filePath = path.join(__dirname, 'source', 'about', 'index.md');
} else if (fileName === 'friend/index.md') {
filePath = path.join(__dirname, 'source', 'friend', 'index.md');
} else {
filePath = path.join(CONFIG.postsDir, fileName);
}
return fs.existsSync(filePath);
}

// 主函数
async function main() {
console.log('🚀 开始获取issues并生成Hexo文章...');

// 检查token
if (CONFIG.token === 'YOUR_TOKEN_HERE') {
console.error('❌ 请设置环境变量 BLOG_CNB_ISSUE_TOKEN 或修改CONFIG.token');
process.exit(1);
}

// 确保posts目录存在
if (!fs.existsSync(CONFIG.postsDir)) {
fs.mkdirSync(CONFIG.postsDir, {recursive: true});
console.log(`📁 创建目录: ${CONFIG.postsDir}`);
}

console.log(`🔗 API地址: ${CONFIG.baseUrl}`);
console.log(`📁 文章保存目录: ${CONFIG.postsDir}`);
console.log(`🏷️ 分类规则: 以"分类:"开头的标签将作为分类(自动移除前缀)`);

let page = 1;
let totalProcessed = 0;
let totalSkipped = 0;
let totalErrors = 0;

try {
while (true) {
// 获取当前页的issues
const issues = await getIssues(page);

if (!issues || issues.length === 0) {
console.log('✅ 没有更多issues,处理完成');
break;
}

console.log(`📄 第 ${page} 页找到 ${issues.length} 个issues`);

// 处理每个issue
for (const issue of issues) {
try {
// 检查文件是否已存在
let fileName;
if (issue.title.trim() === '关于') {
fileName = 'about/index.md';
} else if (issue.title.trim() === '友情链接') {
fileName = 'friend/index.md';
} else if (issue.title.trim() === '友情链接详情') {
// 友情链接详情不需要检查文件存在性,直接处理
fileName = null;
} else {
fileName = `post-${issue.number}.md`;
}

if (fileName && fileExists(fileName)) {
console.log(`🔄 覆盖已存在的文件: ${fileName}`);
}

// 获取详情
const detail = await getIssueDetail(issue.number);
if (!detail) {
totalErrors++;
continue;
}

// 创建文章文件
const success = await createPostFile(detail);
if (success) {
totalProcessed++;
} else {
totalErrors++;
}

} catch (error) {
console.error(`处理issue #${issue.number} 时出错:`, error.message);
totalErrors++;
}
}

page++;
}

console.log('\n📊 处理完成统计:');
console.log(`✅ 成功处理: ${totalProcessed} 篇文章`);
console.log(`⏭️ 跳过已存在: ${totalSkipped} 篇文章`);
console.log(`❌ 处理失败: ${totalErrors} 篇文章`);
console.log(`📁 文章保存在: ${CONFIG.postsDir}`);

} catch (error) {
console.error('❌ 主程序执行出错:', error.message);
process.exit(1);
}
}

// 如果直接运行此脚本
if (require.main === module) {
main();
}

module.exports = {
main, getIssues, getIssueDetail, createPostFile, parseLabels
};
赞助
请作者喝杯咖啡吧
微信

微信

Powered By hexo-theme-reimu
  • CNB
  • Hexo
  • 自动化工具
Git 相关配置
前一篇

Git 相关配置

2016-2025 IT小强
基于 Hexo  Theme.Reimu
晋ICP备16001883号

文章目录

  1. 1. 功能特性
  2. 2. 安装和使用
    1. 2.1. 1. 安装依赖
    2. 2.2. 2. 设置Token
    3. 2.3. 2. 运行工具
  3. 3. 生成的文章格式
  4. 4. 标签和分类处理
    1. 4.1. 示例
  5. 5. 友情链接处理
    1. 5.1. 友情链接页面
    2. 5.2. 友情链接详情
      1. 5.2.1. 支持的格式
      2. 5.2.2. 转换规则
      3. 5.2.3. 生成的YAML格式
  6. 6. 文件命名规则
  7. 7. 配置选项
    1. 7.1. 环境变量配置
    2. 7.2. 代码配置
  8. 8. 注意事项
  9. 9. 输出示例
  10. 10. 故障排除
    1. 10.1. Token错误
    2. 10.2. 网络错误
    3. 10.3. 权限错误
  11. 11. 完整代码
IT小强
IT小强
while(!success) { try(); } // 直到破墙而出
文章
6
分类
3
标签
9

首页

归档

关于

友链