/** * ═══════════════════════════════════════════════════════════ * AGE OS · GitHub API 客户端 * ═══════════════════════════════════════════════════════════ * * 编号: ZY-GITHUB-001 * 签发: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 * * GitHub API 读写客户端 — 用于网站对接 GitHub 仓库 * 使用原生 https 模块(与 cos.js 保持一致,无额外依赖) */ 'use strict'; const https = require('https'); // ─── 配置 ─── const GITHUB_CONFIG = { token: process.env.ZY_GITHUB_PAT || '', owner: process.env.ZY_GITHUB_OWNER || 'qinfendebingshuo', repo: process.env.ZY_GITHUB_REPO || 'guanghulab', apiBase: 'api.github.com' }; // ─── 通用 GitHub API 请求 ─── function githubRequest(method, path, body) { return new Promise((resolve, reject) => { const headers = { 'User-Agent': 'ZY-AGE-OS/1.0', 'Accept': 'application/vnd.github.v3+json' }; if (GITHUB_CONFIG.token) { headers['Authorization'] = `Bearer ${GITHUB_CONFIG.token}`; } if (body) { headers['Content-Type'] = 'application/json'; } const bodyStr = body ? JSON.stringify(body) : null; if (bodyStr) { headers['Content-Length'] = Buffer.byteLength(bodyStr); } const req = https.request({ hostname: GITHUB_CONFIG.apiBase, port: 443, path, method, headers, timeout: 30000 }, (res) => { const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => { const raw = Buffer.concat(chunks).toString(); if (res.statusCode >= 200 && res.statusCode < 300) { try { resolve({ statusCode: res.statusCode, data: JSON.parse(raw) }); } catch { resolve({ statusCode: res.statusCode, data: raw }); } } else { let errorMsg; try { const errObj = JSON.parse(raw); errorMsg = errObj.message || raw.substring(0, 200); } catch { errorMsg = raw.substring(0, 200); } reject(new Error(`GitHub API ${method} ${path}: ${res.statusCode} - ${errorMsg}`)); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('GitHub API request timeout')); }); if (bodyStr) req.write(bodyStr); req.end(); }); } // ═══════════════════════════════════════════════════════════ // 仓库内容操作 // ═══════════════════════════════════════════════════════════ /** * 安全编码文件路径(保留斜杠,编码各段) */ function encodeFilePath(filePath) { return filePath.split('/').map(segment => encodeURIComponent(segment)).join('/'); } /** * 读取文件内容 * @param {string} filePath - 文件路径(相对仓库根目录) * @param {string} [ref] - 分支/标签/SHA(默认 main) * @returns {Promise<{path: string, content: string, sha: string, size: number}>} */ async function readFile(filePath, ref) { const { owner, repo } = GITHUB_CONFIG; let apiPath = `/repos/${owner}/${repo}/contents/${encodeFilePath(filePath)}`; if (ref) apiPath += `?ref=${encodeURIComponent(ref)}`; const { data } = await githubRequest('GET', apiPath); if (data.type !== 'file') { throw new Error(`${filePath} 不是文件,类型为: ${data.type}`); } return { path: data.path, content: Buffer.from(data.content, 'base64').toString('utf8'), sha: data.sha, size: data.size, download_url: data.download_url }; } /** * 列出目录内容 * @param {string} dirPath - 目录路径(空字符串 = 根目录) * @param {string} [ref] - 分支/标签/SHA * @returns {Promise<{path: string, items: object[]}>} */ async function listDirectory(dirPath, ref) { const { owner, repo } = GITHUB_CONFIG; const encodedPath = dirPath ? encodeFilePath(dirPath) : ''; let apiPath = `/repos/${owner}/${repo}/contents/${encodedPath}`; if (ref) apiPath += `?ref=${encodeURIComponent(ref)}`; const { data } = await githubRequest('GET', apiPath); if (!Array.isArray(data)) { throw new Error(`${dirPath} 不是目录`); } return { path: dirPath || '/', items: data.map(item => ({ name: item.name, path: item.path, type: item.type, size: item.size, sha: item.sha })) }; } /** * 创建或更新文件 * @param {string} filePath - 文件路径 * @param {string} content - 文件内容 * @param {string} message - 提交信息 * @param {string} [sha] - 更新时需要当前文件 SHA * @param {string} [branch] - 目标分支 * @returns {Promise<{path: string, sha: string, commit_sha: string}>} */ async function writeFile(filePath, content, message, sha, branch) { const { owner, repo } = GITHUB_CONFIG; const apiPath = `/repos/${owner}/${repo}/contents/${encodeFilePath(filePath)}`; const body = { message: message || `[铸渊] 更新 ${filePath}`, content: Buffer.from(content).toString('base64') }; if (sha) body.sha = sha; if (branch) body.branch = branch; const { data } = await githubRequest('PUT', apiPath, body); return { path: data.content.path, sha: data.content.sha, commit_sha: data.commit.sha }; } // ═══════════════════════════════════════════════════════════ // 仓库信息 // ═══════════════════════════════════════════════════════════ /** * 获取仓库基本信息 * @returns {Promise} */ async function getRepoInfo() { const { owner, repo } = GITHUB_CONFIG; const { data } = await githubRequest('GET', `/repos/${owner}/${repo}`); return { full_name: data.full_name, description: data.description, default_branch: data.default_branch, private: data.private, stargazers_count: data.stargazers_count, forks_count: data.forks_count, open_issues_count: data.open_issues_count, pushed_at: data.pushed_at, updated_at: data.updated_at, size: data.size, language: data.language }; } /** * 获取最近提交 * @param {number} [count] - 获取数量(最大100) * @param {string} [branch] - 分支名 * @returns {Promise} */ async function getRecentCommits(count, branch) { const { owner, repo } = GITHUB_CONFIG; const limit = Math.min(count || 10, 100); let apiPath = `/repos/${owner}/${repo}/commits?per_page=${limit}`; if (branch) apiPath += `&sha=${encodeURIComponent(branch)}`; const { data } = await githubRequest('GET', apiPath); return data.map(c => ({ sha: c.sha.substring(0, 7), full_sha: c.sha, message: c.commit.message.split('\n')[0], author: c.commit.author.name, date: c.commit.author.date })); } /** * 获取 Issues 列表 * @param {string} [state] - open / closed / all * @param {number} [count] - 获取数量 * @param {string[]} [labels] - 标签过滤 * @returns {Promise} */ async function getIssues(state, count, labels) { const { owner, repo } = GITHUB_CONFIG; const limit = Math.min(count || 10, 100); let apiPath = `/repos/${owner}/${repo}/issues?per_page=${limit}&state=${state || 'open'}`; if (labels && labels.length > 0) { apiPath += `&labels=${encodeURIComponent(labels.join(','))}`; } const { data } = await githubRequest('GET', apiPath); return data.map(issue => ({ number: issue.number, title: issue.title, state: issue.state, author: issue.user.login, labels: issue.labels.map(l => l.name), created_at: issue.created_at, updated_at: issue.updated_at, is_pull_request: !!issue.pull_request })); } /** * 获取 Pull Requests 列表 * @param {string} [state] - open / closed / all * @param {number} [count] - 获取数量 * @returns {Promise} */ async function getPullRequests(state, count) { const { owner, repo } = GITHUB_CONFIG; const limit = Math.min(count || 10, 100); const apiPath = `/repos/${owner}/${repo}/pulls?per_page=${limit}&state=${state || 'open'}`; const { data } = await githubRequest('GET', apiPath); return data.map(pr => ({ number: pr.number, title: pr.title, state: pr.state, author: pr.user.login, head: pr.head.ref, base: pr.base.ref, created_at: pr.created_at, updated_at: pr.updated_at, merged_at: pr.merged_at, draft: pr.draft })); } /** * 触发 GitHub Actions 工作流 * @param {string} workflowFile - 工作流文件名(如 deploy-to-zhuyuan-server.yml) * @param {string} [ref] - 触发分支 * @param {object} [inputs] - 工作流输入参数 * @returns {Promise<{triggered: boolean}>} */ async function triggerWorkflow(workflowFile, ref, inputs) { const { owner, repo } = GITHUB_CONFIG; const apiPath = `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowFile)}/dispatches`; const body = { ref: ref || 'main' }; if (inputs) body.inputs = inputs; await githubRequest('POST', apiPath, body); return { triggered: true, workflow: workflowFile }; } // ═══════════════════════════════════════════════════════════ // 连接检查 // ═══════════════════════════════════════════════════════════ /** * 检查 GitHub API 连接状态 * @returns {Promise<{connected: boolean, rate_limit?: object, error?: string}>} */ async function checkConnection() { if (!GITHUB_CONFIG.token) { return { connected: false, reason: 'ZY_GITHUB_PAT 未配置' }; } try { const { data } = await githubRequest('GET', '/rate_limit'); return { connected: true, rate_limit: { limit: data.rate.limit, remaining: data.rate.remaining, reset: new Date(data.rate.reset * 1000).toISOString() } }; } catch (err) { return { connected: false, error: err.message }; } } /** * 获取配置信息(不含敏感信息) */ function getConfig() { return { token_configured: !!GITHUB_CONFIG.token, owner: GITHUB_CONFIG.owner, repo: GITHUB_CONFIG.repo }; } module.exports = { readFile, listDirectory, writeFile, getRepoInfo, getRecentCommits, getIssues, getPullRequests, triggerWorkflow, checkConnection, getConfig, GITHUB_CONFIG };