This commit is contained in:
Vaica committed 2026-03-13 12:24:47 +08:00
1 parent c3e59be13c
commit ee0d17661e
2 files changed
+361 -68

No files matched your search

+26 -7
View File
@@ -5,17 +5,25 @@ on:
branches: branches:
- main # 根据您的实际情况修改分支名称 - main # 根据您的实际情况修改分支名称
schedule: schedule:
- cron: '0 1 */2 * *' # 北京时间每2天(48小时)早上9点执行 - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
env:
TZ: Asia/Shanghai
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Hugo - name: Setup Hugo
uses: peaceiris/actions-hugo@v2 uses: peaceiris/actions-hugo@v2
@@ -42,34 +50,42 @@ jobs:
run: npm ci run: npm ci
- name: Optimize Images - name: Optimize Images
continue-on-error: true
run: node scripts/optimize_images.js run: node scripts/optimize_images.js
- name: Generate AI Comments - name: Generate AI Comments
continue-on-error: true
env: env:
SILICONFLOW_API_KEY: ${{ secrets.SILICONFLOW_API_KEY }} SILICONFLOW_API_KEY: ${{ secrets.SILICONFLOW_API_KEY }}
AI_PROVIDER: siliconflow AI_PROVIDER: siliconflow
run: node scripts/ai_comment.js run: node scripts/ai_comment.js
- name: Commit Optimized Images and AI Comments - name: Commit Optimized Images and AI Comments
continue-on-error: true
run: | run: |
git config --global user.name "GitHub Actions" git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com" git config --global user.email "actions@github.com"
git add content/post git add content/post
if ! git diff --staged --quiet; then if ! git diff --staged --quiet; then
git commit -m "chore: auto-optimize images and generate ai comments [skip ci]" git commit -m "chore: auto-optimize images and generate ai comments [skip ci]"
git push git fetch origin main
git rebase origin/main
git push origin HEAD:main
else else
echo "No changes to commit" echo "No changes to commit"
fi fi
- name: Generate Friend Circle Data - name: Generate Friend Circle Data
continue-on-error: true
run: node scripts/generate_circle_data.js run: node scripts/generate_circle_data.js
- name: Check Link Status - name: Check Link Status
continue-on-error: true
run: node scripts/check_links.js run: node scripts/check_links.js
- name: Read Failed Links Summary - name: Read Failed Links Summary
continue-on-error: true
if: always() if: always()
run: | run: |
if [ -f failed_links_summary.txt ]; then if [ -f failed_links_summary.txt ]; then
@@ -89,6 +105,7 @@ jobs:
./scripts/deploy_edgeone.sh hugo-blog ./public overseas ./scripts/deploy_edgeone.sh hugo-blog ./public overseas
- name: Debug Environment Variables - name: Debug Environment Variables
continue-on-error: true
run: | run: |
echo "Bucket: ${{ secrets.UPYUN_BUCKET }}" echo "Bucket: ${{ secrets.UPYUN_BUCKET }}"
echo "Operator: ${{ secrets.UPYUN_OPERATOR }}" echo "Operator: ${{ secrets.UPYUN_OPERATOR }}"
@@ -117,20 +134,22 @@ jobs:
run: pip install requests run: pip install requests
- name: Refresh DogeCloud CDN - name: Refresh DogeCloud CDN
continue-on-error: true
env: env:
DOGECLOUD_ACCESS_KEY: ${{ secrets.DOGECLOUD_ACCESS_KEY }} DOGECLOUD_ACCESS_KEY: ${{ secrets.DOGECLOUD_ACCESS_KEY }}
DOGECLOUD_SECRET_KEY: ${{ secrets.DOGECLOUD_SECRET_KEY }} DOGECLOUD_SECRET_KEY: ${{ secrets.DOGECLOUD_SECRET_KEY }}
CDN_URL_LIST: ${{ secrets.CDN_URL_LIST }} CDN_URL_LIST: ${{ secrets.CDN_URL_LIST }}
run: python scripts/RefreshCDN.py run: python scripts/RefreshCDN.py
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d %H:%M:%S')"
- name: Generate Notification Report - name: Generate Notification Report
continue-on-error: true
run: node scripts/generate_notification_report.js run: node scripts/generate_notification_report.js
- name: Send deployment success notification - name: Get current date (CN)
id: date
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
- name: Send deployment notification
if: success() if: success()
uses: dawidd6/action-send-mail@v3 uses: dawidd6/action-send-mail@v3
with: with:
+335 -61
View File
@@ -1,30 +1,91 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const matter = require('gray-matter'); 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 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 LINK_SUMMARY_PATH = path.join(__dirname, '../failed_links_summary.txt');
const POSTS_DIR = path.join(__dirname, '../content/post'); const POSTS_DIR = path.join(__dirname, '../content/post');
const HUGO_CONFIG_PATH = path.join(__dirname, '../hugo.toml');
const BASE_URL = "https://usj.cc";
// Helper to format date as YYYYMMDD
function formatDateYYYYMMDDHHmm(date) { function formatDateYYYYMMDDHHmm(date) {
const y = date.getFullYear(); // 获取年份(4位) const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0'); // 月份(补零到2位,注意月份从0开始) const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0'); // 日期(补零到2位) const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0'); // 小时(24小时制,补零到2位) const h = String(date.getHours()).padStart(2, '0');
const mm = String(date.getMinutes()).padStart(2, '0'); // 分钟(补零到2位) const mm = String(date.getMinutes()).padStart(2, '0');
return `${y}${m}${d}${h}${mm}`; return `${y}${m}${d}${h}${mm}`;
} }
// Helper to get latest post from content/post function isWithinDays(date, days) {
function getLatestPost() { if (!(date instanceof Date) || Number.isNaN(date.getTime())) return false;
try { const cutoff = new Date();
if (!fs.existsSync(POSTS_DIR)) return null; cutoff.setDate(cutoff.getDate() - days);
return date >= cutoff;
}
let latestFile = null; function stripHtml(input) {
let latestDate = new Date(0); 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) { function scanDir(dir) {
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir);
@@ -34,60 +95,245 @@ function getLatestPost() {
if (stat.isDirectory()) { if (stat.isDirectory()) {
scanDir(fullPath); scanDir(fullPath);
} else if (file.endsWith('.md')) { } else if (file.endsWith('.md')) {
// Try to read frontmatter date
try { try {
const content = fs.readFileSync(fullPath, 'utf8'); const content = fs.readFileSync(fullPath, 'utf8');
const parsed = matter(content); const parsed = matter(content);
if (parsed.data.date) { const isPublic = parsed.data.status !== 'hidden' && parsed.data.draft !== true;
const date = new Date(parsed.data.date); if (!isPublic) continue;
// Only consider posts that are NOT hidden
if (parsed.data.status !== 'hidden' && parsed.data.draft !== true) { const date = parsed.data.date ? parseDateFlexible(parsed.data.date) : null;
if (date > latestDate) { const title = parsed.data.title ? String(parsed.data.title) : file.replace('.md', '');
latestDate = date; const slugRaw = parsed.data.slug != null ? String(parsed.data.slug).trim() : '';
latestFile = { const slug = slugRaw || (date && !Number.isNaN(date.getTime()) ? formatDateYYYYMMDDHHmm(date) : '');
title: parsed.data.title || file.replace('.md', ''), posts.push({
date: date, title,
// Construct URL based on hugo.toml: /:year:month:day.html date,
url: `${BASE_URL}/${formatDateYYYYMMDDHHmm(date)}.html` slug
}; });
}
}
}
} catch (e) { } catch (e) {
// Ignore parsing errors
} }
} }
} }
} }
scanDir(POSTS_DIR); scanDir(POSTS_DIR);
return latestFile; 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) { } catch (e) {
console.error('Error finding latest post:', e);
return null; 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() { async function main() {
let report = ""; let report = "";
// 1. 最新文章 const nowStr = new Date().toLocaleString();
const latestPost = getLatestPost(); const tz = process.env.TZ || '';
if (latestPost) { const repo = process.env.GITHUB_REPOSITORY || '';
report += `### 📝 最新文章\n`; const refName = process.env.GITHUB_REF_NAME || '';
const dateStr = latestPost.date.toLocaleDateString(); const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 7) : '';
report += `- [${latestPost.title}](${latestPost.url}) (${dateStr})\n`; const runId = process.env.GITHUB_RUN_ID || '';
report += `\n`; const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
} else { const runUrl = repo && runId ? `${serverUrl}/${repo}/actions/runs/${runId}` : '';
report += `### 📝 最新文章\n(未找到公开文章)\n\n`;
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) {
} }
// 2. 友链检测
try { try {
if (fs.existsSync(LINK_SUMMARY_PATH)) { if (fs.existsSync(LINK_SUMMARY_PATH)) {
const linkSummary = fs.readFileSync(LINK_SUMMARY_PATH, 'utf8').trim(); const linkSummary = fs.readFileSync(LINK_SUMMARY_PATH, 'utf8').trim();
report += `### 🔗 友链检测\n`; report += `### 🔗 友链检测\n`;
report += `${linkSummary}\n\n`; const linkMeta = getFileMetaString(LINK_SUMMARY_PATH);
if (linkMeta) report += `- 结果: ${linkMeta}\n`;
report += `${linkSummary || '(无异常)'}\n\n`;
} else { } else {
report += `### 🔗 友链检测\n(无检测结果)\n\n`; report += `### 🔗 友链检测\n(无检测结果)\n\n`;
} }
@@ -95,35 +341,63 @@ async function main() {
console.error("Error reading link summary:", e); console.error("Error reading link summary:", e);
} }
// 3. 近三天朋友圈
try { try {
if (fs.existsSync(FRIEND_CIRCLE_PATH)) { if (fs.existsSync(FRIEND_CIRCLE_PATH)) {
const data = JSON.parse(fs.readFileSync(FRIEND_CIRCLE_PATH, 'utf8')); const data = JSON.parse(fs.readFileSync(FRIEND_CIRCLE_PATH, 'utf8'));
const threeDaysAgo = new Date(); const sevenDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const recentPosts = data.posts.filter(post => new Date(post.date) > threeDaysAgo); 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());
report += `### 👥 朋友圈近况 (近3天)\n`; const recentPosts = allPostsSorted.filter(post => post.__date > sevenDaysAgo);
if (recentPosts.length > 0) {
// Limit to top 10 to avoid too long email const authorSet = new Set(recentPosts.map(p => p.author).filter(Boolean));
const limitedPosts = recentPosts.slice(0, 10);
limitedPosts.forEach(post => { report += `### 👥 朋友圈近况 (近7天)\n`;
const dateStr = new Date(post.date).toLocaleDateString(); const circleMeta = getFileMetaString(FRIEND_CIRCLE_PATH);
report += `- [${post.author}] [${post.title}](${post.link}) (${dateStr})\n`; if (circleMeta) report += `- 数据: ${circleMeta}\n`;
}); report += `- 更新: ${recentPosts.length} 条, 活跃作者: ${authorSet.size}\n`;
if (recentPosts.length > 10) { const toShow = recentPosts.length > 0 ? recentPosts : allPostsSorted;
report += `... 以及其他 ${recentPosts.length - 10} 篇\n`; if (toShow.length === 0) {
}
} else {
report += `无更新\n`; 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`;
} }
report += `\n`;
} }
} catch (e) { } catch (e) {
console.error("Error reading friend circle data:", 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 // Output to stdout for debugging
console.log(report); console.log(report);