From 2c6bd1c9647f1ef58f80027729edff7f67dc3e82 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:56:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96github=20action=20=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 75 ++---- .../index.md | 36 ++- scripts/RefreshCDN.py | 72 ----- scripts/ai_comment.js | 129 --------- scripts/check_links.js | 166 ------------ scripts/generate_notification_report.js | 254 +----------------- scripts/refresh_cdn.js | 61 +++++ themes/Ying/assets/css/main.css | 31 --- themes/Ying/assets/js/modules/artalk.js | 45 +--- themes/Ying/layouts/partials/artalk.html | 8 +- write/src/components/PostEditor.tsx | 9 +- write/src/lib/posts.ts | 7 - 12 files changed, 118 insertions(+), 775 deletions(-) delete mode 100644 scripts/RefreshCDN.py delete mode 100644 scripts/ai_comment.js delete mode 100644 scripts/check_links.js create mode 100644 scripts/refresh_cdn.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 94ac0a07..c131f41e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,9 +3,9 @@ name: Deploy to Production on: push: branches: - - main # 根据您的实际情况修改分支名称 + - main schedule: - - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00 + - cron: '0 1 * * *' # UTC 01:00 = 北京时间 09:00 concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -34,11 +34,10 @@ jobs: - name: Run Pre-Build Scripts shell: pwsh run: | - # 自动处理隐藏文章 (防止 status: hidden 的文章意外泄露) ./scripts/add_draft_to_hidden.ps1 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' @@ -50,43 +49,18 @@ jobs: continue-on-error: true run: node scripts/optimize_images.js - - name: Generate AI Comments - 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 + - name: Commit Optimized Images continue-on-error: true run: | git config --global user.name "GitHub Actions" git config --global user.email "actions@github.com" git add content/post if ! git diff --staged --quiet; then - git commit -m "chore: auto-optimize images and generate ai comments [skip ci]" - git fetch origin main - git rebase origin/main - git push origin HEAD:main + git commit -m "chore: auto-optimize images [skip ci]" else echo "No changes to commit" 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<> $GITHUB_ENV - cat failed_links_summary.txt >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - fi - - name: Build run: rm -rf public && hugo --minify @@ -97,34 +71,14 @@ jobs: chmod +x scripts/deploy_edgeone.sh ./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 uses: her-cat/upyun-deployer@v1.0.3 with: bucket: ${{ secrets.UPYUN_BUCKET }} operator: ${{ secrets.UPYUN_OPERATOR }} password: ${{ secrets.UPYUN_PASSWORD }} - dir: 'public' # 确认这个目录是您的构建输出目录 - publish_dir: '/' # 发布到 UpYun 的根目录 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - - name: Install dependencies - run: pip install requests + dir: 'public' + publish_dir: '/' - name: Refresh DogeCloud CDN continue-on-error: true @@ -132,7 +86,18 @@ jobs: DOGECLOUD_ACCESS_KEY: ${{ secrets.DOGECLOUD_ACCESS_KEY }} DOGECLOUD_SECRET_KEY: ${{ secrets.DOGECLOUD_SECRET_KEY }} 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 continue-on-error: true diff --git a/content/post/2026/2026-06-01-尝试接入-telegram-机器人/index.md b/content/post/2026/2026-06-01-尝试接入-telegram-机器人/index.md index 1beefce4..148741d7 100644 --- a/content/post/2026/2026-06-01-尝试接入-telegram-机器人/index.md +++ b/content/post/2026/2026-06-01-尝试接入-telegram-机器人/index.md @@ -9,38 +9,46 @@ tags: - telegram author: 小赵 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 跑了一遍,别说,效果还真像那么回事 ![image](1780330451237-AgACAgQA.jpg) +不需要太复杂的功能,主要是解决一些机械性的痛点,其实大部分操作交互式的操作反而是不如网页操作的 + ![image](1780330453301-AgACAgQA.jpg) ![image](1780330454866-AgACAgQA.jpg) -我的服务是跑在本地的,写完文章,我岂不是还得下床关电脑,这也太不优雅了 +实际上我的服务是跑在本地的,因为这玩意也没必要跑在公网 -于是 +一顿框框框写完,擦!电脑还没关! + +我岂不是还得下床关电脑,这也太不优雅了! + +于是,写了个远程关机重启需求 ![image](1780330606155-AgACAgQA.jpg) -这下舒服了,各位小伙伴晚安🌙 +这下终于舒服了 -本文由 telegram 提供技术支持 - -小赵 6.2 日 写 +当然,本文也是由 telegram bot 交互式撰写 diff --git a/scripts/RefreshCDN.py b/scripts/RefreshCDN.py deleted file mode 100644 index 2237d782..00000000 --- a/scripts/RefreshCDN.py +++ /dev/null @@ -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() diff --git a/scripts/ai_comment.js b/scripts/ai_comment.js deleted file mode 100644 index c43d6a0c..00000000 --- a/scripts/ai_comment.js +++ /dev/null @@ -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); diff --git a/scripts/check_links.js b/scripts/check_links.js deleted file mode 100644 index 63a7641b..00000000 --- a/scripts/check_links.js +++ /dev/null @@ -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); -}); diff --git a/scripts/generate_notification_report.js b/scripts/generate_notification_report.js index 11d51b33..6ca3bff3 100644 --- a/scripts/generate_notification_report.js +++ b/scripts/generate_notification_report.js @@ -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`); diff --git a/scripts/refresh_cdn.js b/scripts/refresh_cdn.js new file mode 100644 index 00000000..72ecaecb --- /dev/null +++ b/scripts/refresh_cdn.js @@ -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(); diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css index 2e843b3d..57664a06 100644 --- a/themes/Ying/assets/css/main.css +++ b/themes/Ying/assets/css/main.css @@ -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 */ .lazyload { opacity: 1; diff --git a/themes/Ying/assets/js/modules/artalk.js b/themes/Ying/assets/js/modules/artalk.js index 8486cb2e..fac76dc0 100644 --- a/themes/Ying/assets/js/modules/artalk.js +++ b/themes/Ying/assets/js/modules/artalk.js @@ -236,7 +236,7 @@ var processBadges = function() { // Count per-user comments on current page 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'); if (!nickEl) return; var name = (nickEl.textContent || '').trim(); @@ -258,7 +258,7 @@ var processBadges = function() { } 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 avatarEl = wrap.querySelector('.atk-avatar'); if (!nickEl) return; @@ -350,36 +350,6 @@ artalk.on('list-loaded', function() { badgesInjected = false; 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 = ` -
-
- ${ai.name} -
-
-
- ${ai.name} - ${window.artalkConfig.aiBadge || 'AI'} - ${ai.date} -
-
-
${ai.content}
-
-
-
- `; - 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 var list = document.querySelector('.atk-list'); if (list) { @@ -387,7 +357,7 @@ artalk.on('list-loaded', function() { } // 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'); 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) { 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) { diff --git a/themes/Ying/layouts/partials/artalk.html b/themes/Ying/layouts/partials/artalk.html index 13fd7e48..1f3d8563 100644 --- a/themes/Ying/layouts/partials/artalk.html +++ b/themes/Ying/layouts/partials/artalk.html @@ -136,15 +136,9 @@ el: '#Comments', server: '{{ .Site.Params.artalk.server }}', site: '{{ .Site.Params.artalk.site }}', - aiBadge: '{{ .Site.Params.artalk.ai_badge | default "AI" }}', pageKey: '{{ .RelPermalink }}', emoticons: '{{ .Site.Params.artalk.emoticons | absURL }}', - aiComment: {{ if .Params.ai_comment }} - {{ $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 }} + aiComment: null }; // Execute Initialization immediately if function exists (PJAX re-execution), diff --git a/write/src/components/PostEditor.tsx b/write/src/components/PostEditor.tsx index 3f5cdfe7..ffa31aaf 100644 --- a/write/src/components/PostEditor.tsx +++ b/write/src/components/PostEditor.tsx @@ -28,7 +28,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps) categories: initialData?.frontMatter.categories?.join("、") || "", tags: initialData?.frontMatter.tags?.join("、") || "", author: initialData?.frontMatter.author || "小赵", - ai_comment: initialData?.frontMatter.ai_comment || "", }); const [content, setContent] = useState(initialData?.content || ""); const [uploading, setUploading] = useState(false); @@ -61,7 +60,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps) if (!form.title && !content) return; try { 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, dirPath, savedAt: Date.now(), @@ -174,7 +173,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps) author: form.author, layout: "post", }; - if (form.ai_comment) frontMatter.ai_comment = form.ai_comment; const url = isEditing ? `/api/posts/${initialData!.frontMatter.slug}` : "/api/posts"; const method = isEditing ? "PUT" : "POST"; @@ -257,7 +255,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps) const handleApplyMeta = (data: { summary?: string; tags?: string }) => { if (data.tags) setForm((f) => ({ ...f, tags: data.tags || "" })); - if (data.summary) setForm((f) => ({ ...f, ai_comment: data.summary || "" })); }; const inputCls = @@ -332,10 +329,6 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps) setForm({ ...form, tags: e.target.value })} className={inputCls} placeholder="用顿号分隔" /> -
- - setForm({ ...form, ai_comment: e.target.value })} className={inputCls} placeholder="可选" /> -
{/* Folder name preview */} diff --git a/write/src/lib/posts.ts b/write/src/lib/posts.ts index cb8b7f9f..1dfecdee 100644 --- a/write/src/lib/posts.ts +++ b/write/src/lib/posts.ts @@ -13,7 +13,6 @@ export interface PostFrontMatter { tags?: string[]; author?: string; layout?: string; - ai_comment?: string; [key: string]: unknown; } @@ -181,10 +180,6 @@ export function createPost(frontMatter: PostFrontMatter, content: string): strin layout: frontMatter.layout || "post", }; - if (frontMatter.ai_comment) { - fm.ai_comment = frontMatter.ai_comment; - } - const fileContent = matter.stringify(content, fm); const indexPath = path.join(dirPath, "index.md"); fs.writeFileSync(indexPath, fileContent, "utf-8"); @@ -227,7 +222,6 @@ export function updatePost( author: merged.author || "", layout: merged.layout || "post", }; - if (merged.ai_comment) fm.ai_comment = merged.ai_comment; const fileContent = matter.stringify(content ?? post.content, fm); fs.writeFileSync(path.join(newDirPath, "index.md"), fileContent, "utf-8"); @@ -245,7 +239,6 @@ export function updatePost( author: merged.author || "", layout: merged.layout || "post", }; - if (merged.ai_comment) fm.ai_comment = merged.ai_comment; const fileContent = matter.stringify(content ?? post.content, fm); fs.writeFileSync(post.filePath, fileContent, "utf-8");