feat: initialize VitePress CMS

This commit is contained in:
Coldsmile_7
2026-06-05 23:21:41 +08:00
commit 928f742d5e
45 changed files with 8214 additions and 0 deletions
+266
View File
@@ -0,0 +1,266 @@
import type { DocPage, GitHubConnection, SiteSettings } from "../types";
import { parseFrontmatter, parseSiteSettings, stringifyMarkdownPage, stringifySiteConfig } from "../utils/siteContent";
interface GitTreeResponse {
tree: Array<{
path: string;
type: "blob" | "tree";
sha: string;
}>;
}
interface GitHubContentResponse {
content: string;
encoding: string;
sha: string;
path: string;
}
function githubHeaders(connection: GitHubConnection) {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
if (connection.token.trim()) {
headers.Authorization = `Bearer ${connection.token.trim()}`;
}
return headers;
}
function usesSessionProxy(connection: GitHubConnection) {
return !connection.token.trim();
}
function siteParams(connection: GitHubConnection) {
return new URLSearchParams({
owner: connection.owner,
repo: connection.repo,
branch: connection.branch,
siteRoot: connection.siteRoot,
});
}
function normalizeSiteRoot(siteRoot: string) {
return siteRoot.trim().replace(/^\/+|\/+$/g, "");
}
function toRepoPath(connection: GitHubConnection, path: string) {
const siteRoot = normalizeSiteRoot(connection.siteRoot);
return siteRoot ? `${siteRoot}/${path}` : path;
}
function fromRepoPath(connection: GitHubConnection, path: string) {
const siteRoot = normalizeSiteRoot(connection.siteRoot);
return siteRoot && path.startsWith(`${siteRoot}/`) ? path.slice(siteRoot.length + 1) : path;
}
function decodeBase64Content(content: string) {
const binary = atob(content.replace(/\n/g, ""));
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
function encodeBase64Content(content: string) {
const bytes = new TextEncoder().encode(content);
let binary = "";
bytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return btoa(binary);
}
async function githubFetch<T>(connection: GitHubConnection, path: string, init?: RequestInit) {
const response = await fetch(`https://api.github.com${path}`, {
...init,
headers: {
...githubHeaders(connection),
...init?.headers,
},
});
if (!response.ok) {
const details = await response.text();
throw new Error(`GitHub 请求失败:${response.status} ${details}`);
}
return (await response.json()) as T;
}
async function getContent(connection: GitHubConnection, repoPath: string) {
const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/");
return githubFetch<GitHubContentResponse>(
connection,
`/repos/${connection.owner}/${connection.repo}/contents/${encodedPath}?ref=${encodeURIComponent(connection.branch)}`,
);
}
export async function loadGitHubPages(connection: GitHubConnection) {
if (usesSessionProxy(connection)) {
const response = await fetch(`/api/github/site/pages?${siteParams(connection).toString()}`);
if (!response.ok) {
throw new Error(`读取 GitHub 页面失败:${response.status}`);
}
const data = (await response.json()) as { pages: DocPage[] };
return data.pages;
}
const tree = await githubFetch<GitTreeResponse>(
connection,
`/repos/${connection.owner}/${connection.repo}/git/trees/${encodeURIComponent(connection.branch)}?recursive=1`,
);
const siteRoot = normalizeSiteRoot(connection.siteRoot);
const prefix = siteRoot ? `${siteRoot}/` : "";
const markdownFiles = tree.tree
.filter((entry) => entry.type === "blob")
.filter((entry) => entry.path.endsWith(".md"))
.filter((entry) => !entry.path.includes("/node_modules/"))
.filter((entry) => entry.path !== `${prefix}README.md`)
.filter((entry) => !prefix || entry.path.startsWith(prefix));
return Promise.all(
markdownFiles.sort((a, b) => a.path.localeCompare(b.path)).map(async (entry) => {
const file = await getContent(connection, entry.path);
const source = decodeBase64Content(file.content);
const parsed = parseFrontmatter(source);
const path = fromRepoPath(connection, entry.path);
return {
id: path,
path,
sha: file.sha,
title: parsed.title || path.replace(/\.md$/, ""),
description: parsed.description,
content: parsed.content,
} satisfies DocPage;
}),
);
}
export async function saveGitHubPage(connection: GitHubConnection, page: DocPage) {
if (usesSessionProxy(connection)) {
const response = await fetch("/api/github/site/pages", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
site: {
owner: connection.owner,
repo: connection.repo,
branch: connection.branch,
siteRoot: connection.siteRoot,
},
page,
}),
});
if (!response.ok) {
throw new Error(`保存 GitHub 页面失败:${response.status}`);
}
const data = (await response.json()) as { sha: string };
return data.sha;
}
const repoPath = toRepoPath(connection, page.path);
const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/");
const response = await githubFetch<{ content: { sha: string } }>(
connection,
`/repos/${connection.owner}/${connection.repo}/contents/${encodedPath}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: `docs: update ${page.path} from VitePress-CMS`,
content: encodeBase64Content(stringifyMarkdownPage(page)),
branch: connection.branch,
sha: page.sha,
}),
},
);
return response.content.sha;
}
export async function loadGitHubSettings(connection: GitHubConnection) {
if (usesSessionProxy(connection)) {
const response = await fetch(`/api/github/site/settings?${siteParams(connection).toString()}`);
if (!response.ok) {
throw new Error(`读取 GitHub 配置失败:${response.status}`);
}
return (await response.json()) as {
settings: SiteSettings;
sha: string;
};
}
const configPath = toRepoPath(connection, ".vitepress/config.ts");
const file = await getContent(connection, configPath);
const source = decodeBase64Content(file.content);
return {
settings: parseSiteSettings(source),
sha: file.sha,
};
}
export async function saveGitHubSettings(
connection: GitHubConnection,
settings: SiteSettings,
sha?: string,
) {
if (usesSessionProxy(connection)) {
const response = await fetch("/api/github/site/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
site: {
owner: connection.owner,
repo: connection.repo,
branch: connection.branch,
siteRoot: connection.siteRoot,
},
settings,
sha,
}),
});
if (!response.ok) {
throw new Error(`保存 GitHub 配置失败:${response.status}`);
}
const data = (await response.json()) as { sha: string };
return data.sha;
}
const repoPath = toRepoPath(connection, ".vitepress/config.ts");
const encodedPath = repoPath.split("/").map(encodeURIComponent).join("/");
const response = await githubFetch<{ content: { sha: string } }>(
connection,
`/repos/${connection.owner}/${connection.repo}/contents/${encodedPath}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "docs: update VitePress config from VitePress-CMS",
content: encodeBase64Content(stringifySiteConfig(settings)),
branch: connection.branch,
sha,
}),
},
);
return response.content.sha;
}