优化github action 逻辑
This commit is contained in:
1 parent
f91be983d4
commit
2c6bd1c964
12 files changed
+116
-773
No files matched your search
@@ -3,7 +3,7 @@ name: Deploy to Production
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main # 根据您的实际情况修改分支名称
|
- main
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
|
- cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -34,11 +34,10 @@ jobs:
|
|||||||
- name: Run Pre-Build Scripts
|
- name: Run Pre-Build Scripts
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
# 自动处理隐藏文章 (防止 status: hidden 的文章意外泄露)
|
|
||||||
./scripts/add_draft_to_hidden.ps1
|
./scripts/add_draft_to_hidden.ps1
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -50,43 +49,18 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: node scripts/optimize_images.js
|
run: node scripts/optimize_images.js
|
||||||
|
|
||||||
- name: Generate AI Comments
|
- name: Commit Optimized Images
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
SILICONFLOW_API_KEY: ${{ secrets.SILICONFLOW_API_KEY }}
|
|
||||||
AI_PROVIDER: siliconflow
|
|
||||||
run: node scripts/ai_comment.js
|
|
||||||
|
|
||||||
- name: Commit Optimized Images and AI Comments
|
|
||||||
continue-on-error: true
|
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 [skip ci]"
|
||||||
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: Check Link Status
|
|
||||||
continue-on-error: true
|
|
||||||
run: node scripts/check_links.js
|
|
||||||
|
|
||||||
- name: Read Failed Links Summary
|
|
||||||
continue-on-error: true
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ -f failed_links_summary.txt ]; then
|
|
||||||
echo "LINK_SUMMARY<<EOF" >> $GITHUB_ENV
|
|
||||||
cat failed_links_summary.txt >> $GITHUB_ENV
|
|
||||||
echo "EOF" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: rm -rf public && hugo --minify
|
run: rm -rf public && hugo --minify
|
||||||
|
|
||||||
@@ -97,34 +71,14 @@ jobs:
|
|||||||
chmod +x scripts/deploy_edgeone.sh
|
chmod +x scripts/deploy_edgeone.sh
|
||||||
./scripts/deploy_edgeone.sh hugo-blog ./public overseas
|
./scripts/deploy_edgeone.sh hugo-blog ./public overseas
|
||||||
|
|
||||||
- name: Debug Environment Variables
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "Bucket: ${{ secrets.UPYUN_BUCKET }}"
|
|
||||||
echo "Operator: ${{ secrets.UPYUN_OPERATOR }}"
|
|
||||||
# 不推荐打印密码,但可以确认是否为空字符串
|
|
||||||
if [ -z "${{ secrets.UPYUN_PASSWORD }}" ]; then
|
|
||||||
echo "Password is empty or not set"
|
|
||||||
else
|
|
||||||
echo "Password is set"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Deploy to UpYun
|
- name: Deploy to UpYun
|
||||||
uses: her-cat/upyun-deployer@v1.0.3
|
uses: her-cat/upyun-deployer@v1.0.3
|
||||||
with:
|
with:
|
||||||
bucket: ${{ secrets.UPYUN_BUCKET }}
|
bucket: ${{ secrets.UPYUN_BUCKET }}
|
||||||
operator: ${{ secrets.UPYUN_OPERATOR }}
|
operator: ${{ secrets.UPYUN_OPERATOR }}
|
||||||
password: ${{ secrets.UPYUN_PASSWORD }}
|
password: ${{ secrets.UPYUN_PASSWORD }}
|
||||||
dir: 'public' # 确认这个目录是您的构建输出目录
|
dir: 'public'
|
||||||
publish_dir: '/' # 发布到 UpYun 的根目录
|
publish_dir: '/'
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pip install requests
|
|
||||||
|
|
||||||
- name: Refresh DogeCloud CDN
|
- name: Refresh DogeCloud CDN
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -132,7 +86,18 @@ jobs:
|
|||||||
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: node scripts/refresh_cdn.js
|
||||||
|
|
||||||
|
- name: Push Image Optimizations
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
git fetch origin main
|
||||||
|
git rebase origin/main
|
||||||
|
if ! git log --oneline -1 | grep -q "\[skip ci\]"; then
|
||||||
|
echo "No auto-commits to push"
|
||||||
|
else
|
||||||
|
git push origin HEAD:main
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Generate Notification Report
|
- name: Generate Notification Report
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -9,38 +9,46 @@ tags:
|
|||||||
- telegram
|
- telegram
|
||||||
author: 小赵
|
author: 小赵
|
||||||
layout: post
|
layout: post
|
||||||
ai_comment: >-
|
|
||||||
技术债的迭代过程总是充满这种螺旋式上升的幽默感——从手动改JSON到Git冲突再到自动化机器人,最后发现自动化也带来了新的"下床关电脑"的痛点。这种持续优化的思维正是工程师精神的体现,不过建议考虑把本地服务容器化后部署到云端,彻底解决优雅入睡问题。Telegram机器人作为CLI的渐进式替代方案,确实是个不错的中间态选择。
|
|
||||||
---
|
---
|
||||||
最近有位博友给我发邮件,他的博客更换了域名,需要改一下友链的链接
|
最近有位博友给我发邮件,他的博客更换了域名,需要改一下友链的链接
|
||||||
|
|
||||||
果然男人过了二十五就是五十二了,记忆力严重下降
|
果然男人过了二十五就是五十二了
|
||||||
|
|
||||||
在 hugo 的项目根目录下咋都没找到友联的 json 文件,因为之前懒的缘故,我也没有在 readme 文件记录,还好,最后翻看了之前的屎山源码,打开 vscode,一套行云流水,替换了友联数据
|
在 hugo 的项目根目录下咋都没找到友联的 json 文件
|
||||||
|
|
||||||
然后 git push,发现冲突了,原来笔记本本地文件好久没有同步了,现在本地文件过于旧了,先于是先 pull 再 push,搞定!
|
因为之前懒的缘故,我也没有在 readme 记录,最后翻看了之前的屎山源码,终于找到了散落在角落的 json 数据
|
||||||
|
|
||||||
晚上躺床上,心想,这也太不优雅了!
|
然后 git push,发现冲突了,又折腾一会儿,处理 git 版本冲突,原来笔记本本地文件好久没有同步了
|
||||||
|
|
||||||
于是,决定把他接入 telegram 机器人,起初就想实现个改友联和改订阅链接,后面想想干脆把写文章也加进来,也许这样还能增加点更新频率,不是吗,哈哈
|
深圳这鬼天气是真的热,晚上躺床上,翻来覆去睡不着
|
||||||
|
|
||||||
于是,通过写 md 文件,通过 todo 的形式,把需求扔给 ai,别说,效果还真像那么回事
|
转念一想,这样管理友联订阅数据,也太不优雅了!
|
||||||
|
|
||||||
|
之前在学校的时候做 Java 项目研究过 telegram bot
|
||||||
|
|
||||||
|
我决定把他接入 telegram bot,起初就想实现个改友联和改订阅链接,后面想想干脆把写文章也加进来,也许这样还能增加点便利性,下班回家,谁还愿意坐电脑桌呢
|
||||||
|
|
||||||
|
于是,通过写需求文档,把需求拆成 todo ,先确认需求,再做需求,避免不必要的 token 浪费,给claude code 跑了一遍,别说,效果还真像那么回事
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
不需要太复杂的功能,主要是解决一些机械性的痛点,其实大部分操作交互式的操作反而是不如网页操作的
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
我的服务是跑在本地的,写完文章,我岂不是还得下床关电脑,这也太不优雅了
|
实际上我的服务是跑在本地的,因为这玩意也没必要跑在公网
|
||||||
|
|
||||||
于是
|
一顿框框框写完,擦!电脑还没关!
|
||||||
|
|
||||||
|
我岂不是还得下床关电脑,这也太不优雅了!
|
||||||
|
|
||||||
|
于是,写了个远程关机重启需求
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
这下舒服了,各位小伙伴晚安🌙
|
这下终于舒服了
|
||||||
|
|
||||||
本文由 telegram 提供技术支持
|
当然,本文也是由 telegram bot 交互式撰写
|
||||||
|
|
||||||
小赵 6.2 日 写
|
|
||||||
|
|
||||||
@@ -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()
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -1,52 +1,16 @@
|
|||||||
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 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');
|
|
||||||
|
|
||||||
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) {
|
function parseDateFlexible(input) {
|
||||||
if (input instanceof Date) return Number.isNaN(input.getTime()) ? null : input;
|
if (input instanceof Date) return Number.isNaN(input.getTime()) ? null : input;
|
||||||
if (input == null) return null;
|
if (input == null) return null;
|
||||||
|
|
||||||
const raw = String(input).trim();
|
const raw = String(input).trim();
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
|
|
||||||
const direct = new Date(raw);
|
const direct = new Date(raw);
|
||||||
if (!Number.isNaN(direct.getTime())) return direct;
|
if (!Number.isNaN(direct.getTime())) return direct;
|
||||||
|
|
||||||
const digits = raw.replace(/\D/g, '');
|
const digits = raw.replace(/\D/g, '');
|
||||||
if (digits.length === 14 || digits.length === 12) {
|
if (digits.length === 14 || digits.length === 12) {
|
||||||
const y = Number(digits.slice(0, 4));
|
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);
|
const dt = new Date(y, mo - 1, d, h, mi, s);
|
||||||
return Number.isNaN(dt.getTime()) ? null : dt;
|
return Number.isNaN(dt.getTime()) ? null : dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (digits.length === 8) {
|
if (digits.length === 8) {
|
||||||
const y = Number(digits.slice(0, 4));
|
const y = Number(digits.slice(0, 4));
|
||||||
const mo = Number(digits.slice(4, 6));
|
const mo = Number(digits.slice(4, 6));
|
||||||
const d = Number(digits.slice(6, 8));
|
const d = Number(digits.slice(6, 8));
|
||||||
if ([y, mo, d].some(n => !Number.isFinite(n))) return null;
|
if ([y, mo, d].some(n => !Number.isFinite(n))) return null;
|
||||||
const dt = new Date(y, mo - 1, d);
|
return new Date(y, mo - 1, d);
|
||||||
return Number.isNaN(dt.getTime()) ? null : dt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateMDY(date) {
|
function isWithinDays(date, days) {
|
||||||
try {
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return false;
|
||||||
return date.toLocaleDateString('en-US');
|
const cutoff = new Date();
|
||||||
} catch (e) {
|
cutoff.setDate(cutoff.getDate() - days);
|
||||||
return '';
|
return date >= cutoff;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectPublicPosts() {
|
function collectPublicPosts() {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(POSTS_DIR)) return [];
|
if (!fs.existsSync(POSTS_DIR)) return [];
|
||||||
|
|
||||||
const posts = [];
|
const posts = [];
|
||||||
|
|
||||||
function scanDir(dir) {
|
function scanDir(dir) {
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dir);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -97,24 +55,16 @@ function collectPublicPosts() {
|
|||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(fullPath, 'utf8');
|
const content = fs.readFileSync(fullPath, 'utf8');
|
||||||
const parsed = matter(content);
|
const parsed = matter(content);
|
||||||
const isPublic = parsed.data.status !== 'hidden' && parsed.data.draft !== true;
|
if (parsed.data.status === 'hidden' || parsed.data.draft === true) continue;
|
||||||
if (!isPublic) continue;
|
|
||||||
|
|
||||||
const date = parsed.data.date ? parseDateFlexible(parsed.data.date) : null;
|
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({
|
posts.push({
|
||||||
title,
|
title: parsed.data.title ? String(parsed.data.title) : file.replace('.md', ''),
|
||||||
date,
|
date,
|
||||||
slug
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) { /* skip */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
scanDir(POSTS_DIR);
|
scanDir(POSTS_DIR);
|
||||||
return posts;
|
return posts;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -123,124 +73,6 @@ 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() {
|
async function main() {
|
||||||
let report = "";
|
let report = "";
|
||||||
|
|
||||||
@@ -276,79 +108,13 @@ async function main() {
|
|||||||
if (undatedCount > 0) report += `- 缺少日期: ${undatedCount}\n`;
|
if (undatedCount > 0) report += `- 缺少日期: ${undatedCount}\n`;
|
||||||
report += `\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 += `### 🧩 环境\n`;
|
||||||
report += `- Node: ${process.version}\n`;
|
report += `- Node: ${process.version}\n`;
|
||||||
if (process.env.RUNNER_OS) report += `- Runner: ${process.env.RUNNER_OS}\n`;
|
if (process.env.RUNNER_OS) report += `- Runner: ${process.env.RUNNER_OS}\n`;
|
||||||
report += `\n`;
|
report += `\n`;
|
||||||
|
|
||||||
// Output to stdout for debugging
|
|
||||||
console.log(report);
|
console.log(report);
|
||||||
|
|
||||||
// Output to GITHUB_ENV if running in Actions
|
|
||||||
if (process.env.GITHUB_ENV) {
|
if (process.env.GITHUB_ENV) {
|
||||||
const delimiter = `EOF_${Math.random().toString(36).substr(2, 9)}`;
|
const delimiter = `EOF_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
fs.appendFileSync(process.env.GITHUB_ENV, `DEPLOY_REPORT<<${delimiter}\n${report}\n${delimiter}\n`);
|
fs.appendFileSync(process.env.GITHUB_ENV, `DEPLOY_REPORT<<${delimiter}\n${report}\n${delimiter}\n`);
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -4994,37 +4994,6 @@ html[data-theme="light"] .atk-loading-spinner svg circle {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* AI Comment — subtle badge only */
|
|
||||||
.atk-comment.atk-ai-comment .atk-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(135deg, #c9a84c, #a07d30);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 1px 3px rgba(160, 125, 48, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.atk-comment.atk-ai-comment .atk-nick {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.atk-comment.atk-ai-comment .atk-avatar img {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
.atk-comment.atk-ai-comment:hover .atk-avatar img {
|
|
||||||
transform: rotate(6deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .atk-comment.atk-ai-comment .atk-badge {
|
|
||||||
color: #1a1a1a;
|
|
||||||
box-shadow: 0 1px 4px rgba(160, 125, 48, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lazy Load Animation */
|
/* Lazy Load Animation */
|
||||||
.lazyload {
|
.lazyload {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ var processBadges = function() {
|
|||||||
|
|
||||||
// Count per-user comments on current page
|
// Count per-user comments on current page
|
||||||
var pageCounts = {};
|
var pageCounts = {};
|
||||||
document.querySelectorAll('.atk-comment-wrap:not(.atk-ai-comment)').forEach(function(wrap) {
|
document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) {
|
||||||
var nickEl = wrap.querySelector('.atk-nick');
|
var nickEl = wrap.querySelector('.atk-nick');
|
||||||
if (!nickEl) return;
|
if (!nickEl) return;
|
||||||
var name = (nickEl.textContent || '').trim();
|
var name = (nickEl.textContent || '').trim();
|
||||||
@@ -258,7 +258,7 @@ var processBadges = function() {
|
|||||||
}
|
}
|
||||||
saveUserCounts(data);
|
saveUserCounts(data);
|
||||||
|
|
||||||
document.querySelectorAll('.atk-comment-wrap:not(.atk-ai-comment)').forEach(function(wrap) {
|
document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) {
|
||||||
var nickEl = wrap.querySelector('.atk-nick');
|
var nickEl = wrap.querySelector('.atk-nick');
|
||||||
var avatarEl = wrap.querySelector('.atk-avatar');
|
var avatarEl = wrap.querySelector('.atk-avatar');
|
||||||
if (!nickEl) return;
|
if (!nickEl) return;
|
||||||
@@ -350,36 +350,6 @@ artalk.on('list-loaded', function() {
|
|||||||
badgesInjected = false;
|
badgesInjected = false;
|
||||||
setTimeout(injectAllBadges, 300);
|
setTimeout(injectAllBadges, 300);
|
||||||
|
|
||||||
if (window.artalkConfig.aiComment) {
|
|
||||||
var ai = window.artalkConfig.aiComment;
|
|
||||||
var listBody = document.querySelector('.atk-list .atk-list-body');
|
|
||||||
if (listBody && !listBody.querySelector('.atk-ai-comment')) {
|
|
||||||
var aiCommentHtml = `
|
|
||||||
<div class="atk-comment-wrap atk-comment atk-ai-comment" id="atk-comment-ai">
|
|
||||||
<div class="atk-avatar" uk-scrollspy="cls: uk-animation-scale-up; delay: 200">
|
|
||||||
<img src="${ai.avatar}" alt="${ai.name}">
|
|
||||||
</div>
|
|
||||||
<div class="atk-main" uk-scrollspy="cls: uk-animation-slide-right-small; delay: 300">
|
|
||||||
<div class="atk-header">
|
|
||||||
<span class="atk-nick" uk-scrollspy="cls: uk-animation-slide-top-small; delay: 350">${ai.name}</span>
|
|
||||||
<span class="atk-badge">${window.artalkConfig.aiBadge || 'AI'}</span>
|
|
||||||
<span class="atk-date">${ai.date}</span>
|
|
||||||
</div>
|
|
||||||
<div class="atk-body">
|
|
||||||
<div class="atk-content">${ai.content}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
listBody.insertAdjacentHTML('afterbegin', aiCommentHtml);
|
|
||||||
|
|
||||||
var noComment = document.querySelector('.atk-list-no-comment');
|
|
||||||
if (noComment) {
|
|
||||||
noComment.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add staggered animation to the comment list
|
// Add staggered animation to the comment list
|
||||||
var list = document.querySelector('.atk-list');
|
var list = document.querySelector('.atk-list');
|
||||||
if (list) {
|
if (list) {
|
||||||
@@ -387,7 +357,7 @@ artalk.on('list-loaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply granular animations to inner elements
|
// Apply granular animations to inner elements
|
||||||
document.querySelectorAll('.atk-comment-wrap:not(.atk-ai-comment)').forEach(function(wrap) {
|
document.querySelectorAll('.atk-comment-wrap').forEach(function(wrap) {
|
||||||
var avatar = wrap.querySelector('.atk-avatar');
|
var avatar = wrap.querySelector('.atk-avatar');
|
||||||
if (avatar) avatar.setAttribute('uk-scrollspy', 'cls: uk-animation-scale-up; delay: 200');
|
if (avatar) avatar.setAttribute('uk-scrollspy', 'cls: uk-animation-scale-up; delay: 200');
|
||||||
|
|
||||||
@@ -402,15 +372,6 @@ artalk.on('list-loaded', function() {
|
|||||||
if (window.lazyLoadAgain) {
|
if (window.lazyLoadAgain) {
|
||||||
window.lazyLoadAgain();
|
window.lazyLoadAgain();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force hide "No Comment" if AI comment exists
|
|
||||||
if (window.artalkConfig.aiComment) {
|
|
||||||
var noComment = document.querySelector('.atk-list-no-comment');
|
|
||||||
var aiComment = document.querySelector('.atk-ai-comment');
|
|
||||||
if (aiComment && noComment) {
|
|
||||||
noComment.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -136,15 +136,9 @@
|
|||||||
el: '#Comments',
|
el: '#Comments',
|
||||||
server: '{{ .Site.Params.artalk.server }}',
|
server: '{{ .Site.Params.artalk.server }}',
|
||||||
site: '{{ .Site.Params.artalk.site }}',
|
site: '{{ .Site.Params.artalk.site }}',
|
||||||
aiBadge: '{{ .Site.Params.artalk.ai_badge | default "AI" }}',
|
|
||||||
pageKey: '{{ .RelPermalink }}',
|
pageKey: '{{ .RelPermalink }}',
|
||||||
emoticons: '{{ .Site.Params.artalk.emoticons | absURL }}',
|
emoticons: '{{ .Site.Params.artalk.emoticons | absURL }}',
|
||||||
aiComment: {{ if .Params.ai_comment }}
|
aiComment: null
|
||||||
{{ $avatar := .Site.Params.ai.avatar }}
|
|
||||||
{{ if not $avatar }}{{ $avatar = .Site.Params.author.avatar }}{{ end }}
|
|
||||||
{{ if not $avatar }}{{ $avatar = "https://cravatar.cn/avatar/unknown?d=mp" }}{{ end }}
|
|
||||||
{{ dict "content" .Params.ai_comment "avatar" $avatar "name" (.Site.Params.ai.name | default "AI Assistant") "date" (.Date.Format "2006-01-02") | jsonify | safeJS }}
|
|
||||||
{{ else }}null{{ end }}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute Initialization immediately if function exists (PJAX re-execution),
|
// Execute Initialization immediately if function exists (PJAX re-execution),
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
|||||||
categories: initialData?.frontMatter.categories?.join("、") || "",
|
categories: initialData?.frontMatter.categories?.join("、") || "",
|
||||||
tags: initialData?.frontMatter.tags?.join("、") || "",
|
tags: initialData?.frontMatter.tags?.join("、") || "",
|
||||||
author: initialData?.frontMatter.author || "小赵",
|
author: initialData?.frontMatter.author || "小赵",
|
||||||
ai_comment: initialData?.frontMatter.ai_comment || "",
|
|
||||||
});
|
});
|
||||||
const [content, setContent] = useState(initialData?.content || "");
|
const [content, setContent] = useState(initialData?.content || "");
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@@ -61,7 +60,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
|||||||
if (!form.title && !content) return;
|
if (!form.title && !content) return;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(cacheKey, JSON.stringify({
|
localStorage.setItem(cacheKey, JSON.stringify({
|
||||||
form: { title: form.title, slug: form.slug, date: form.date, categories: form.categories, tags: form.tags, author: form.author, ai_comment: form.ai_comment },
|
form: { title: form.title, slug: form.slug, date: form.date, categories: form.categories, tags: form.tags, author: form.author },
|
||||||
content,
|
content,
|
||||||
dirPath,
|
dirPath,
|
||||||
savedAt: Date.now(),
|
savedAt: Date.now(),
|
||||||
@@ -174,7 +173,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
|||||||
author: form.author,
|
author: form.author,
|
||||||
layout: "post",
|
layout: "post",
|
||||||
};
|
};
|
||||||
if (form.ai_comment) frontMatter.ai_comment = form.ai_comment;
|
|
||||||
|
|
||||||
const url = isEditing ? `/api/posts/${initialData!.frontMatter.slug}` : "/api/posts";
|
const url = isEditing ? `/api/posts/${initialData!.frontMatter.slug}` : "/api/posts";
|
||||||
const method = isEditing ? "PUT" : "POST";
|
const method = isEditing ? "PUT" : "POST";
|
||||||
@@ -257,7 +255,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
|||||||
|
|
||||||
const handleApplyMeta = (data: { summary?: string; tags?: string }) => {
|
const handleApplyMeta = (data: { summary?: string; tags?: string }) => {
|
||||||
if (data.tags) setForm((f) => ({ ...f, tags: data.tags || "" }));
|
if (data.tags) setForm((f) => ({ ...f, tags: data.tags || "" }));
|
||||||
if (data.summary) setForm((f) => ({ ...f, ai_comment: data.summary || "" }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputCls =
|
const inputCls =
|
||||||
@@ -332,10 +329,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
|
|||||||
<label className="block text-[12px] font-medium text-text/70 mb-1.5">标签</label>
|
<label className="block text-[12px] font-medium text-text/70 mb-1.5">标签</label>
|
||||||
<input type="text" value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} className={inputCls} placeholder="用顿号分隔" />
|
<input type="text" value={form.tags} onChange={(e) => setForm({ ...form, tags: e.target.value })} className={inputCls} placeholder="用顿号分隔" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-[12px] font-medium text-text/70 mb-1.5">AI 评论</label>
|
|
||||||
<input type="text" value={form.ai_comment} onChange={(e) => setForm({ ...form, ai_comment: e.target.value })} className={inputCls} placeholder="可选" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Folder name preview */}
|
{/* Folder name preview */}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export interface PostFrontMatter {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
author?: string;
|
author?: string;
|
||||||
layout?: string;
|
layout?: string;
|
||||||
ai_comment?: string;
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,10 +180,6 @@ export function createPost(frontMatter: PostFrontMatter, content: string): strin
|
|||||||
layout: frontMatter.layout || "post",
|
layout: frontMatter.layout || "post",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (frontMatter.ai_comment) {
|
|
||||||
fm.ai_comment = frontMatter.ai_comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileContent = matter.stringify(content, fm);
|
const fileContent = matter.stringify(content, fm);
|
||||||
const indexPath = path.join(dirPath, "index.md");
|
const indexPath = path.join(dirPath, "index.md");
|
||||||
fs.writeFileSync(indexPath, fileContent, "utf-8");
|
fs.writeFileSync(indexPath, fileContent, "utf-8");
|
||||||
@@ -227,7 +222,6 @@ export function updatePost(
|
|||||||
author: merged.author || "",
|
author: merged.author || "",
|
||||||
layout: merged.layout || "post",
|
layout: merged.layout || "post",
|
||||||
};
|
};
|
||||||
if (merged.ai_comment) fm.ai_comment = merged.ai_comment;
|
|
||||||
|
|
||||||
const fileContent = matter.stringify(content ?? post.content, fm);
|
const fileContent = matter.stringify(content ?? post.content, fm);
|
||||||
fs.writeFileSync(path.join(newDirPath, "index.md"), fileContent, "utf-8");
|
fs.writeFileSync(path.join(newDirPath, "index.md"), fileContent, "utf-8");
|
||||||
@@ -245,7 +239,6 @@ export function updatePost(
|
|||||||
author: merged.author || "",
|
author: merged.author || "",
|
||||||
layout: merged.layout || "post",
|
layout: merged.layout || "post",
|
||||||
};
|
};
|
||||||
if (merged.ai_comment) fm.ai_comment = merged.ai_comment;
|
|
||||||
|
|
||||||
const fileContent = matter.stringify(content ?? post.content, fm);
|
const fileContent = matter.stringify(content ?? post.content, fm);
|
||||||
fs.writeFileSync(post.filePath, fileContent, "utf-8");
|
fs.writeFileSync(post.filePath, fileContent, "utf-8");
|
||||||
|
|||||||
Reference in new issue
Block a user