const fs = require('fs'); const path = require('path'); const matter = require('gray-matter'); const axios = require('axios'); const FRIEND_CIRCLE_PATH = path.join(__dirname, '../themes/Ying/static/json/friend_circle_data.json'); const LINK_SUMMARY_PATH = path.join(__dirname, '../failed_links_summary.txt'); const POSTS_DIR = path.join(__dirname, '../content/post'); const HUGO_CONFIG_PATH = path.join(__dirname, '../hugo.toml'); function formatDateYYYYMMDDHHmm(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); const h = String(date.getHours()).padStart(2, '0'); const mm = String(date.getMinutes()).padStart(2, '0'); return `${y}${m}${d}${h}${mm}`; } function isWithinDays(date, days) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) return false; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); return date >= cutoff; } function stripHtml(input) { if (input == null) return ''; return String(input) .replace(/<[^>]*>/g, '') .replace(/\s+/g, ' ') .trim(); } function truncate(input, maxLen) { const s = input == null ? '' : String(input); if (s.length <= maxLen) return s; return `${s.slice(0, Math.max(0, maxLen - 1))}…`; } function parseDateFlexible(input) { if (input instanceof Date) return Number.isNaN(input.getTime()) ? null : input; if (input == null) return null; const raw = String(input).trim(); if (!raw) return null; const direct = new Date(raw); if (!Number.isNaN(direct.getTime())) return direct; const digits = raw.replace(/\D/g, ''); if (digits.length === 14 || digits.length === 12) { const y = Number(digits.slice(0, 4)); const mo = Number(digits.slice(4, 6)); const d = Number(digits.slice(6, 8)); const h = Number(digits.slice(8, 10)); const mi = Number(digits.slice(10, 12)); const s = digits.length === 14 ? Number(digits.slice(12, 14)) : 0; if ([y, mo, d, h, mi, s].some(n => !Number.isFinite(n))) return null; const dt = new Date(y, mo - 1, d, h, mi, s); return Number.isNaN(dt.getTime()) ? null : dt; } if (digits.length === 8) { const y = Number(digits.slice(0, 4)); const mo = Number(digits.slice(4, 6)); const d = Number(digits.slice(6, 8)); if ([y, mo, d].some(n => !Number.isFinite(n))) return null; const dt = new Date(y, mo - 1, d); return Number.isNaN(dt.getTime()) ? null : dt; } return null; } function formatDateMDY(date) { try { return date.toLocaleDateString('en-US'); } catch (e) { return ''; } } function collectPublicPosts() { try { if (!fs.existsSync(POSTS_DIR)) return []; const posts = []; function scanDir(dir) { const files = fs.readdirSync(dir); for (const file of files) { const fullPath = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { scanDir(fullPath); } else if (file.endsWith('.md')) { try { const content = fs.readFileSync(fullPath, 'utf8'); const parsed = matter(content); const isPublic = parsed.data.status !== 'hidden' && parsed.data.draft !== true; if (!isPublic) continue; const date = parsed.data.date ? parseDateFlexible(parsed.data.date) : null; const title = parsed.data.title ? String(parsed.data.title) : file.replace('.md', ''); const slugRaw = parsed.data.slug != null ? String(parsed.data.slug).trim() : ''; const slug = slugRaw || (date && !Number.isNaN(date.getTime()) ? formatDateYYYYMMDDHHmm(date) : ''); posts.push({ title, date, slug }); } catch (e) { } } } } scanDir(POSTS_DIR); return posts; } catch (e) { console.error('Error collecting posts:', e); return []; } } function getFileMetaString(filePath) { try { const stat = fs.statSync(filePath); const mtime = stat.mtime ? stat.mtime.toLocaleString() : ''; const sizeKb = typeof stat.size === 'number' ? Math.max(1, Math.round(stat.size / 1024)) : 0; return mtime ? `${sizeKb}KB, 更新于 ${mtime}` : `${sizeKb}KB`; } catch (e) { return ''; } } function readHugoConfig() { try { if (!fs.existsSync(HUGO_CONFIG_PATH)) return null; const raw = fs.readFileSync(HUGO_CONFIG_PATH, 'utf8'); const lines = raw.split(/\r?\n/); let baseURL = ''; let inArtalk = false; let artalkServer = ''; let artalkSite = ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (!baseURL) { const mBase = trimmed.match(/^baseURL\s*=\s*"([^"]+)"\s*$/); if (mBase) baseURL = mBase[1]; } if (/^\[params\.artalk\]\s*$/.test(trimmed)) { inArtalk = true; continue; } if (inArtalk && /^\[.*\]\s*$/.test(trimmed)) break; if (!inArtalk) continue; const mServer = trimmed.match(/^server\s*=\s*"([^"]+)"\s*$/); if (mServer) { artalkServer = mServer[1]; continue; } const mSite = trimmed.match(/^site\s*=\s*"([^"]+)"\s*$/); if (mSite) { artalkSite = mSite[1]; } } const conf = {}; if (baseURL) conf.baseURL = baseURL; if (artalkServer && artalkSite) conf.artalk = { server: artalkServer, site: artalkSite }; return Object.keys(conf).length > 0 ? conf : null; } catch (e) { return null; } } function readArtalkConfig() { try { if (!fs.existsSync(HUGO_CONFIG_PATH)) return null; const raw = fs.readFileSync(HUGO_CONFIG_PATH, 'utf8'); const lines = raw.split(/\r?\n/); let inSection = false; let server = ''; let site = ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (/^\[params\.artalk\]\s*$/.test(trimmed)) { inSection = true; continue; } if (inSection && /^\[.*\]\s*$/.test(trimmed)) break; if (!inSection) continue; const mServer = trimmed.match(/^server\s*=\s*"([^"]+)"\s*$/); if (mServer) { server = mServer[1]; continue; } const mSite = trimmed.match(/^site\s*=\s*"([^"]+)"\s*$/); if (mSite) { site = mSite[1]; } } if (!server || !site) return null; return { server, site }; } catch (e) { return null; } } async function fetchArtalkCommentsForPage({ server, site, pageKey, limit }) { const base = String(server || '').replace(/\/+$/, ''); const siteName = String(site || '').trim(); const key = String(pageKey || '').trim(); if (!base || !siteName || !key) return []; const url = `${base}/api/v2/comments`; try { const resp = await axios.get(url, { params: { site_name: siteName, page_key: key, limit: limit || 50, offset: 0 }, timeout: 10000, validateStatus: (s) => s >= 200 && s < 400 }); const payload = resp && resp.data ? resp.data : null; const list = payload && Array.isArray(payload.comments) ? payload.comments : []; return list; } catch (e) { return []; } } async function main() { let report = ""; const nowStr = new Date().toLocaleString(); const tz = process.env.TZ || ''; const repo = process.env.GITHUB_REPOSITORY || ''; const refName = process.env.GITHUB_REF_NAME || ''; const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 7) : ''; const runId = process.env.GITHUB_RUN_ID || ''; const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'; const runUrl = repo && runId ? `${serverUrl}/${repo}/actions/runs/${runId}` : ''; report += `### 🚀 部署信息\n`; report += `- 时间: ${nowStr}\n`; if (tz) report += `- 时区: ${tz}\n`; if (repo) report += `- 仓库: ${repo}\n`; if (refName) report += `- 分支: ${refName}\n`; if (sha) report += `- 提交: ${sha}\n`; if (runUrl) report += `- Actions: ${runUrl}\n`; report += `\n`; const posts = collectPublicPosts(); const totalPublic = posts.length; const dated = posts.filter(p => p.date instanceof Date && !Number.isNaN(p.date.getTime())); const undatedCount = totalPublic - dated.length; const recent7 = dated.filter(p => isWithinDays(p.date, 7)); const recent30 = dated.filter(p => isWithinDays(p.date, 30)); report += `### 📦 内容概览\n`; report += `- 公开文章: ${totalPublic}\n`; report += `- 近7天更新: ${recent7.length}\n`; report += `- 近30天更新: ${recent30.length}\n`; if (undatedCount > 0) report += `- 缺少日期: ${undatedCount}\n`; report += `\n`; try { const hugoConf = readHugoConfig(); const artalk = hugoConf && hugoConf.artalk ? hugoConf.artalk : readArtalkConfig(); const baseURL = hugoConf && hugoConf.baseURL ? String(hugoConf.baseURL).replace(/\/+$/, '') : 'https://usj.cc'; if (artalk) { const postsByDate = posts .filter(p => p && typeof p.slug === 'string' && p.slug.trim()) .filter(p => p.date instanceof Date && !Number.isNaN(p.date.getTime())) .slice() .sort((a, b) => b.date.getTime() - a.date.getTime()) .slice(0, 15); const pageKeys = [...new Set(postsByDate.map(p => `/${String(p.slug).trim()}.html`))]; const allComments = []; for (const pageKey of pageKeys) { const pageComments = await fetchArtalkCommentsForPage({ server: artalk.server, site: artalk.site, pageKey, limit: 100 }); pageComments.forEach(c => { const dateRaw = c && (c.date || c.created_at || c.createdAt || c.time); const date = dateRaw ? new Date(dateRaw) : null; allComments.push({ nick: c && (c.nick || c.nickname || c.name || c.user_name || c.userName) ? (c.nick || c.nickname || c.name || c.user_name || c.userName) : '匿名', content: c && (c.content_marked || c.content || c.text || c.body), date, pageKey, link: `${baseURL}${pageKey}` }); }); } const latest = allComments .filter(c => c.date instanceof Date && !Number.isNaN(c.date.getTime())) .sort((a, b) => b.date.getTime() - a.date.getTime()) .slice(0, 10); report += `### Artalk 评论 (近10条)\n`; report += `- 站点: ${artalk.site}\n`; report += `- 服务: ${artalk.server}\n`; if (latest.length === 0) report += `(无评论或获取失败)\n`; latest.forEach((c) => { const content = truncate(stripHtml(c.content), 60); const dateStr = formatDateMDY(c.date); report += `- [${c.nick}] ${content} (${dateStr}) -> ${c.link}\n`; }); report += `\n`; } } catch (e) { } try { if (fs.existsSync(LINK_SUMMARY_PATH)) { const linkSummary = fs.readFileSync(LINK_SUMMARY_PATH, 'utf8').trim(); report += `### 🔗 友链检测\n`; const linkMeta = getFileMetaString(LINK_SUMMARY_PATH); if (linkMeta) report += `- 结果: ${linkMeta}\n`; report += `${linkSummary || '(无异常)'}\n\n`; } else { report += `### 🔗 友链检测\n(无检测结果)\n\n`; } } catch (e) { console.error("Error reading link summary:", e); } try { if (fs.existsSync(FRIEND_CIRCLE_PATH)) { const data = JSON.parse(fs.readFileSync(FRIEND_CIRCLE_PATH, 'utf8')); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const allPostsSorted = (data.posts || []) .map(p => { const dt = parseDateFlexible(p && p.date); return Object.assign({}, p, { __date: dt }); }) .filter(p => p.__date instanceof Date && !Number.isNaN(p.__date.getTime())) .sort((a, b) => b.__date.getTime() - a.__date.getTime()); const recentPosts = allPostsSorted.filter(post => post.__date > sevenDaysAgo); const authorSet = new Set(recentPosts.map(p => p.author).filter(Boolean)); report += `### 👥 朋友圈近况 (近7天)\n`; const circleMeta = getFileMetaString(FRIEND_CIRCLE_PATH); if (circleMeta) report += `- 数据: ${circleMeta}\n`; report += `- 更新: ${recentPosts.length} 条, 活跃作者: ${authorSet.size}\n`; const toShow = recentPosts.length > 0 ? recentPosts : allPostsSorted; if (toShow.length === 0) { report += `无更新\n`; report += `\n`; } else { if (recentPosts.length === 0) { report += `- 近7天无更新,展示最近10条:\n`; } const limitedPosts = toShow.slice(0, 10); limitedPosts.forEach(post => { const dateStr = formatDateMDY(post.__date); const author = post.author || '未知作者'; const link = post.link || post.url || ''; const blogUrl = post.blogUrl ? ` ${post.blogUrl}` : ''; if (link) { report += `- [${author}] \`${link}\` (${dateStr})${blogUrl}\n`; } else { report += `- [${author}] (${dateStr})${blogUrl}\n`; } }); if (toShow.length > 10) { report += `... 以及其他 ${toShow.length - 10} 篇\n`; } report += `\n`; } } } catch (e) { console.error("Error reading friend circle data:", e); } report += `### 🧩 环境\n`; report += `- Node: ${process.version}\n`; if (process.env.RUNNER_OS) report += `- Runner: ${process.env.RUNNER_OS}\n`; report += `\n`; // Output to stdout for debugging console.log(report); // Output to GITHUB_ENV if running in Actions if (process.env.GITHUB_ENV) { const delimiter = `EOF_${Math.random().toString(36).substr(2, 9)}`; fs.appendFileSync(process.env.GITHUB_ENV, `DEPLOY_REPORT<<${delimiter}\n${report}\n${delimiter}\n`); } } main();