优化github action 逻辑

This commit is contained in:
Vaica committed 2026-06-02 10:56:59 +08:00
1 parent f91be983d4
commit 2c6bd1c964
12 files changed
+118 -775

No files matched your search

-72
View File
@@ -1,72 +0,0 @@
import os
import sys
import json
import requests
import hmac
from hashlib import sha1
import urllib.parse
def dogecloud_api(api_path, data={}, json_mode=False):
access_key = os.environ.get("DOGECLOUD_ACCESS_KEY")
secret_key = os.environ.get("DOGECLOUD_SECRET_KEY")
if not access_key or not secret_key:
print("Error: DOGECLOUD_ACCESS_KEY or DOGECLOUD_SECRET_KEY not found in environment.")
sys.exit(1)
body = ''
mime = ''
if json_mode:
body = json.dumps(data)
mime = 'application/json'
else:
body = urllib.parse.urlencode(data)
mime = 'application/x-www-form-urlencoded'
sign_str = api_path + "\n" + body
signed_data = hmac.new(secret_key.encode('utf-8'), sign_str.encode('utf-8'), sha1)
sign = signed_data.digest().hex()
authorization = 'TOKEN ' + access_key + ':' + sign
try:
response = requests.post('https://api.dogecloud.com' + api_path, data=body, headers={
'Authorization': authorization,
'Content-Type': mime
})
return response.json()
except Exception as e:
print(f"Request failed: {e}")
return None
def main():
cdn_url_list_str = os.environ.get("CDN_URL_LIST")
if not cdn_url_list_str:
print("Warning: CDN_URL_LIST not found or empty.")
return
# Split by comma or newline and filter empty strings
url_list = [url.strip() for url in cdn_url_list_str.replace('\n', ',').split(',') if url.strip()]
if not url_list:
print("No URLs to refresh.")
return
print(f"Refreshing {len(url_list)} URLs...")
# DogeCloud allows batch refresh
api_path = '/cdn/refresh/add.json'
data = {
'rtype': 'path',
'urls': json.dumps(url_list)
}
result = dogecloud_api(api_path, data)
if result and result.get('code') == 200:
print(f"Refresh task submitted successfully. Task ID: {result['data']['task_id']}")
else:
print(f"Failed to submit refresh task. Response: {json.dumps(result)}")
sys.exit(1)
if __name__ == "__main__":
main()
-129
View File
@@ -1,129 +0,0 @@
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const axios = require('axios');
require('dotenv').config();
// Configuration
const CONTENT_DIR = path.join(__dirname, '../content/post');
const PROVIDER = process.env.AI_PROVIDER || 'siliconflow'; // 'siliconflow' or 'zhipu'
const SILICONFLOW_API_KEY = process.env.SILICONFLOW_API_KEY;
const ZHIPU_API_KEY = process.env.ZHIPU_API_KEY;
// SiliconFlow Config
const SILICONFLOW_URL = 'https://api.siliconflow.cn/v1/chat/completions';
const SILICONFLOW_MODEL = 'deepseek-ai/DeepSeek-V3';
// Zhipu Config
const ZHIPU_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/completions';
const ZHIPU_MODEL = 'glm-4';
async function generateComment(content) {
const prompt = `你是"优世界"博客的常驻读者。你的评论风格:
声音:
- 你是一名资深技术人员,评论冷静、精准、有洞察力
- 善于从技术细节中提炼出通用原理,或用简洁的类比让复杂问题变清晰
- 偶尔就事论事地指出文章中可以深入探讨的方向,像同行审阅一样,但不过度严肃
- 不强行搞笑,但可以有一丝冷幽默
格式:
- 100 字以内
- 只输出评论文本,不加引号、不署名、不带 emoji
文章内容:
${content.substring(0, 2000)}`;
try {
if (PROVIDER === 'siliconflow') {
if (!SILICONFLOW_API_KEY) throw new Error('SILICONFLOW_API_KEY is missing');
const response = await axios.post(SILICONFLOW_URL, {
model: SILICONFLOW_MODEL,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 200
}, {
headers: {
'Authorization': `Bearer ${SILICONFLOW_API_KEY}`,
'Content-Type': 'application/json'
}
});
return response.data.choices[0].message.content.trim();
} else if (PROVIDER === 'zhipu') {
if (!ZHIPU_API_KEY) throw new Error('ZHIPU_API_KEY is missing');
const response = await axios.post(ZHIPU_URL, {
model: ZHIPU_MODEL,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7
}, {
headers: {
'Authorization': `Bearer ${ZHIPU_API_KEY}`,
'Content-Type': 'application/json'
}
});
return response.data.choices[0].message.content.trim();
}
} catch (error) {
console.error('API Error:', error.response ? error.response.data : error.message);
return null;
}
}
async function processFiles(dir) {
const files = await fs.promises.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stats = await fs.promises.stat(filePath);
if (stats.isDirectory()) {
await processFiles(filePath);
continue;
}
if (!file.endsWith('.md')) continue;
const content = await fs.promises.readFile(filePath, 'utf8');
const parsed = matter(content);
if (parsed.data.ai_comment && !process.argv.includes('--force')) {
console.log(`[Skip] ${file} already has ai_comment`);
continue;
}
console.log(`[Generating] Comment for ${file}...`);
// Remove front matter and HTML tags for prompt
const cleanContent = parsed.content.replace(/<[^>]*>?/gm, '');
const comment = await generateComment(cleanContent);
if (comment) {
parsed.data.ai_comment = comment;
const newContent = matter.stringify(parsed.content, parsed.data);
await fs.promises.writeFile(filePath, newContent);
console.log(`[Success] Updated ${file}`);
} else {
console.log(`[Failed] Could not generate comment for ${file}`);
}
}
}
// Check arguments
const args = process.argv.slice(2);
if (args.includes('--help')) {
console.log(`
Usage: node scripts/ai_comment.js
Environment Variables (.env):
AI_PROVIDER - 'siliconflow' (default) or 'zhipu'
SILICONFLOW_API_KEY - Key for SiliconFlow
ZHIPU_API_KEY - Key for Zhipu AI
`);
process.exit(0);
}
console.log(`Starting AI Comment Generator using ${PROVIDER}...`);
processFiles(CONTENT_DIR).catch(console.error);
-166
View File
@@ -1,166 +0,0 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const API_BASE = 'https://api.usj.cc';
const HEALTH_URL = `${API_BASE}/api/health`;
const LINKS_URL = `${API_BASE}/api/links`;
const outputPath = path.join(__dirname, '../static/link_status.json');
const summaryPath = path.join(__dirname, '../failed_links_summary.txt');
const BATCH_TIMEOUT = 120000; // /api/health probes all sites server-side
const SINGLE_TIMEOUT = 20000;
const PER_SITE_CONCURRENCY = 8;
if (!fs.existsSync(path.dirname(outputPath))) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
}
const client = axios.create({
timeout: BATCH_TIMEOUT,
validateStatus: (s) => s >= 200 && s < 500,
headers: {
'User-Agent': 'blog-link-health/1.0 (+github-actions)'
}
});
function ensureArray(value) {
return Array.isArray(value) ? value : [];
}
function pickName(map, url) {
if (!url) return '';
return map.get(url) || map.get(url.replace(/\/+$/, '')) || '';
}
function buildNameMap(links) {
const map = new Map();
links.forEach((l) => {
if (!l || !l.url) return;
map.set(l.url, l.name || l.url);
map.set(l.url.replace(/\/+$/, ''), l.name || l.url);
});
return map;
}
async function fetchLinks() {
try {
const resp = await client.get(LINKS_URL, { timeout: SINGLE_TIMEOUT });
return ensureArray(resp.data && resp.data.links);
} catch (e) {
console.warn('[link-check] /api/links failed:', e.message);
return [];
}
}
async function fetchBatchHealth() {
try {
const resp = await client.get(HEALTH_URL, { timeout: BATCH_TIMEOUT });
const data = resp.data;
if (!data || !Array.isArray(data.results)) {
console.warn('[link-check] /api/health returned unexpected payload, falling back to per-site probes');
return null;
}
return data;
} catch (e) {
console.warn('[link-check] /api/health batch failed:', e.message, '- falling back to per-site probes');
return null;
}
}
async function fetchSingleHealth(url) {
try {
const resp = await client.get(HEALTH_URL, {
params: { url },
timeout: SINGLE_TIMEOUT
});
const data = resp.data || {};
return {
url,
status: typeof data.status === 'number' ? data.status : 0,
ok: !!data.ok,
latency: typeof data.latency === 'number' ? data.latency : null,
error: typeof data.error === 'string' ? data.error : ''
};
} catch (e) {
return { url, status: 0, ok: false, latency: null, error: e.message || 'request_failed' };
}
}
async function fetchPerSiteHealth(links) {
const queue = links.filter((l) => l && l.url).map((l) => l.url);
const out = [];
let cursor = 0;
async function worker() {
while (cursor < queue.length) {
const idx = cursor++;
const url = queue[idx];
const result = await fetchSingleHealth(url);
out.push(result);
}
}
const workers = Array(Math.min(queue.length, PER_SITE_CONCURRENCY))
.fill(null)
.map(() => worker());
await Promise.all(workers);
return out;
}
function writeOutputs(results, nameMap) {
const map = {};
const failed = [];
results.forEach((r) => {
if (!r || !r.url) return;
map[r.url] = !!r.ok;
if (!r.ok) {
const name = pickName(nameMap, r.url) || r.url;
const reason = r.error ? r.error : (r.status ? `HTTP ${r.status}` : 'unknown');
failed.push(`- ${name}: ${r.url} (${reason})`);
}
});
fs.writeFileSync(outputPath, JSON.stringify(map, null, 2));
if (failed.length > 0) {
const summary = `\n🚫 监测到 ${failed.length} 个异常友链:\n${failed.join('\n')}\n`;
fs.writeFileSync(summaryPath, summary);
} else {
fs.writeFileSync(summaryPath, '\n✅ 所有友链状态正常\n');
}
return failed.length;
}
async function main() {
const start = Date.now();
console.log('[link-check] starting via', HEALTH_URL);
const linksList = await fetchLinks();
const nameMap = buildNameMap(linksList);
let results = null;
const batch = await fetchBatchHealth();
if (batch) {
results = batch.results;
console.log(`[link-check] batch report: total=${batch.total} alive=${batch.alive} dead=${batch.dead}`);
} else if (linksList.length > 0) {
console.log(`[link-check] per-site probe of ${linksList.length} sites...`);
results = await fetchPerSiteHealth(linksList);
} else {
console.error('[link-check] no links to probe and batch endpoint failed');
process.exit(1);
}
const failedCount = writeOutputs(results, nameMap);
const duration = ((Date.now() - start) / 1000).toFixed(2);
console.log(`[link-check] done in ${duration}s, failed=${failedCount}, status=${outputPath}, summary=${summaryPath}`);
}
main().catch((e) => {
console.error('[link-check] fatal:', e);
process.exit(1);
});
+10 -244
View File
@@ -1,52 +1,16 @@
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const axios = require('axios');
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));
@@ -59,33 +23,27 @@ function parseDateFlexible(input) {
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 new Date(y, mo - 1, d);
}
return null;
}
function formatDateMDY(date) {
try {
return date.toLocaleDateString('en-US');
} catch (e) {
return '';
}
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 collectPublicPosts() {
try {
if (!fs.existsSync(POSTS_DIR)) return [];
const posts = [];
function scanDir(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
@@ -97,24 +55,16 @@ function collectPublicPosts() {
try {
const content = fs.readFileSync(fullPath, 'utf8');
const parsed = matter(content);
const isPublic = parsed.data.status !== 'hidden' && parsed.data.draft !== true;
if (!isPublic) continue;
if (parsed.data.status === 'hidden' || parsed.data.draft === true) 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,
title: parsed.data.title ? String(parsed.data.title) : file.replace('.md', ''),
date,
slug
});
} catch (e) {
}
} catch (e) { /* skip */ }
}
}
}
scanDir(POSTS_DIR);
return posts;
} catch (e) {
@@ -123,127 +73,9 @@ function collectPublicPosts() {
}
}
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 || '';
@@ -276,79 +108,13 @@ async function main() {
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);
}
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`);
+61
View File
@@ -0,0 +1,61 @@
const crypto = require("crypto");
async function dogecloudApi(apiPath, data = {}) {
const accessKey = process.env.DOGECLOUD_ACCESS_KEY;
const secretKey = process.env.DOGECLOUD_SECRET_KEY;
if (!accessKey || !secretKey) {
console.error("Error: DOGECLOUD_ACCESS_KEY or DOGECLOUD_SECRET_KEY not set.");
process.exit(1);
}
const body = JSON.stringify(data);
const signStr = apiPath + "\n" + body;
const sign = crypto.createHmac("sha1", secretKey).update(signStr).digest("hex");
const authorization = `TOKEN ${accessKey}:${sign}`;
const res = await fetch(`https://api.dogecloud.com${apiPath}`, {
method: "POST",
headers: {
Authorization: authorization,
"Content-Type": "application/json",
},
body,
});
return res.json();
}
async function main() {
const cdnUrlListStr = process.env.CDN_URL_LIST;
if (!cdnUrlListStr) {
console.log("Warning: CDN_URL_LIST not found or empty.");
return;
}
const urlList = cdnUrlListStr
.replace(/\n/g, ",")
.split(",")
.map((u) => u.trim())
.filter(Boolean);
if (urlList.length === 0) {
console.log("No URLs to refresh.");
return;
}
console.log(`Refreshing ${urlList.length} URLs...`);
const result = await dogecloudApi("/cdn/refresh/add.json", {
rtype: "path",
urls: JSON.stringify(urlList),
});
if (result && result.code === 200) {
console.log(`Refresh task submitted. Task ID: ${result.data.task_id}`);
} else {
console.error(`Failed to submit refresh task. Response: ${JSON.stringify(result)}`);
process.exit(1);
}
}
main();