xiaomusic/docs/.vitepress/vitepress-plugin-github-issues.mts
2024-12-13 20:52:26 +08:00

261 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// vitepress-plugin-github-issues.mts
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import type { Plugin } from 'vitepress';
interface ReplaceRule {
baseUrl: string; // 要匹配的基地址
targetUrl: string; // 替换后的目标地址
}
interface GitHubIssuesPluginOptions {
repo: string; // GitHub repository info in the format 'owner/repo'
token: string;
replaceRules: ReplaceRule[];
githubProxy: string;
}
async function fetchAllIssues(repo: string, token: string): Promise<any[]> {
const maxRetries = 3; // 最大重试次数
let attempt = 0;
const allIssues: any[] = [];
let page = 1;
while (true) {
while (attempt < maxRetries) {
try {
const response = await axios.get(`https://api.github.com/repos/${repo}/issues`, {
headers: {
Authorization: `token ${token}`
},
params: {
page: page,
per_page: 100 // 每页最多返回100条记录
}
});
// 如果没有更多问题了,退出循环
if (response.data.length === 0) {
return allIssues;
}
allIssues.push(...response.data);
page++; // 下一页
attempt = 0; // 重置尝试次数
break; // 退出尝试循环
} catch (error) {
if (error.response && error.response.status === 503) {
console.error(`服务不可用, 正在重试...`);
attempt++;
const waitTime = Math.pow(2, attempt) * 1000; // 指数等待时间
await new Promise(resolve => setTimeout(resolve, waitTime));
} else {
throw error; // 如果不是503错误抛出错误并停止重试
}
}
}
if (attempt >= maxRetries) {
throw new Error('最大重试次数已达,请检查 API 状态(可能是请求过于频繁)');
}
}
}
async function fetchIssueComments(repo: string, issueNumber: number, token: string): Promise<any[]> {
const maxRetries = 3;
let attempt = 0;
const allComments: any[] = [];
let page = 1;
while (true) {
while (attempt < maxRetries) {
try {
const response = await axios.get(
`https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`,
{
headers: {
Authorization: `token ${token}`,
},
params: {
page: page,
per_page: 100,
},
}
);
if (response.data.length === 0) {
return allComments; // 如果没有更多评论,退出循环
}
allComments.push(...response.data);
page++;
attempt = 0;
break; // 成功获取评论数据,退出重试
} catch (error: any) {
if (error.response && error.response.status === 503) {
console.error('服务不可用,正在重试...');
attempt++;
const waitTime = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
} else {
throw error;
}
}
}
if (attempt >= maxRetries) {
throw new Error('最大重试次数已达,请检查 API 状态(可能是请求过于频繁)');
}
}
}
function clearDirectory(dir: string) {
if (fs.existsSync(dir)) {
fs.readdirSync(dir).forEach((file) => {
const filePath = path.join(dir, file);
if (fs.lstatSync(filePath).isDirectory()) {
clearDirectory(filePath); // 递归清理子目录
fs.rmdirSync(filePath);
} else {
fs.unlinkSync(filePath); // 删除文件
}
});
console.log(`Cleared directory: ${dir}`);
}
}
function copyFile(source: string, destination: string) {
if (fs.existsSync(source)) {
fs.copyFileSync(source, destination);
console.log(`Copied file from ${source} to ${destination}`);
} else {
console.error(`file not found at ${source}`);
}
}
// 在文件开头插入内容
function prependToFile(filePath: string, text: string) {
const content = fs.readFileSync(filePath, 'utf-8');
const updatedContent = `${text}\n\n${content}`;
fs.writeFileSync(filePath, updatedContent, 'utf-8');
console.log(`Prepended text to ${filePath}`);
}
function replaceGithubAssetUrls(content: string, githubProxy: string): string {
const pattern1 = /https:\/\/github\.com\/[^\/]+\/[^\/]+\/assets\/[\w-]+/g;
const pattern2 = /https:\/\/github\.com\/user-attachments\/assets\/[\w-]+/g;
const proxyPrefix = "https://cloudflare-github-proxy.hanxi-info.workers.dev/proxy";
// 使用正则表达式替换符合条件的链接
const transformedContent = content.replace(pattern1, (match) => {
return match.replace("https://github.com", githubProxy);
}).replace(pattern2, (match) => {
return match.replace("https://github.com", githubProxy);
});
return transformedContent;
}
export default function GitHubIssuesPlugin(options: GitHubIssuesPluginOptions): Plugin {
const { repo, token, replaceRules, githubProxy } = options;
return {
name: 'vitepress-plugin-github-issues',
async buildStart() {
try {
const issues = await fetchAllIssues(repo, token);
console.log(`Fetched ${issues.length} issues from GitHub`); // Log the number of issues fetched
const docsDir = path.join(process.cwd(), 'issues');
// 清空 issues 目录
clearDirectory(docsDir);
// Create a directory to store markdown files if it doesn't exist
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir);
console.log(`Created docs directory: ${docsDir}`);
}
// 拷贝 ../README.md 文件到当前目录
const readmeSource = path.join(process.cwd(), '../README.md');
const readmeDestination = path.join(docsDir, 'index.md');
copyFile(readmeSource, readmeDestination);
// 拷贝 ../CHANGELOG.md 文件到当前目录
const changelogSource = path.join(process.cwd(), '../CHANGELOG.md');
const changelogDestination = path.join(docsDir, 'changelog.md');
copyFile(changelogSource, changelogDestination);
prependToFile(changelogDestination, '# 变更日志');
for (const issue of issues) {
// 仅处理包含 "文档" 标签的 issue
const hasDocumentLabel = issue.labels.some(label => label.name === '文档');
if (hasDocumentLabel) {
const title = issue.title.replace(/[\/\\?%*:|"<>]/g, '-');
const fileName = `${issue.number}.md`;
// 获取评论数据
const comments = await fetchIssueComments(repo, issue.number, token);
let content =
`---
title: ${issue.title}
---
# ${title}
${issue.body}
## 评论
`;
// 插入评论
if (comments.length > 0) {
comments.forEach((comment, index) => {
content += `
### 评论 ${index + 1} - ${comment.user.login}
${comment.body}
---
`;
});
} else {
content += "没有评论。\n";
}
replaceRules.forEach(({ baseUrl, targetUrl }) => {
// 将 baseUrl 转换为正则表达式,匹配后接的路径部分
const pattern = new RegExp(`${baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}(/\\d+)`, 'g');
// 替换为目标 URL
content = content.replace(pattern, `${targetUrl}$1.html`);
});
content = replaceGithubAssetUrls(content, githubProxy);
content += `[链接到 GitHub Issue](${issue.html_url})\n`;
const filePath = path.join(docsDir, fileName);
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
console.log(`Created file: ${filePath}`); // Log each created file
} else {
console.log(`Skipped issue: ${issue.title}`); // Log skipped issues
}
}
console.log(`Successfully created markdown files from GitHub issues.`);
} catch (error) {
console.error('Error fetching GitHub issues:', error);
}
},
};
}