feat: add daily tip & case auto-publish system
- GitHub Actions workflow: daily-content.yml (UTC 00:00 / 北京 08:00) - Content generation script: .github/scripts/generate-daily.js (Claude API) - Initial data: website/daily/data.json - website/index.html: new 每日精选 section with bilingual support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
157
.github/scripts/generate-daily.js
vendored
Normal file
157
.github/scripts/generate-daily.js
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 每日内容生成脚本
|
||||
* 调用 Claude API 生成每日小技巧和每日最佳案例
|
||||
* 写入 website/daily/data.json
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DATA_FILE = path.join(__dirname, '../../website/daily/data.json');
|
||||
const MAX_HISTORY = 30;
|
||||
|
||||
const TODAY = new Date().toLocaleDateString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).replace(/\//g, '-');
|
||||
|
||||
const SYSTEM_PROMPT = `你是 U-Claw 虾盘的内容助手。U-Claw 是一款专为中国用户打造的 OpenClaw AI 助手离线安装 U 盘,特点如下:
|
||||
- 完全离线安装,无需翻墙
|
||||
- 支持 macOS / Windows / Linux
|
||||
- 内置 52 个预装技能(Skills)
|
||||
- 支持 8 大国产模型:DeepSeek、Kimi、通义千问、智谱GLM、MiniMax、豆包、千帆、Mimo
|
||||
- 多平台接入:QQ / 飞书 / 钉钉 / 企业微信 / Telegram / Discord
|
||||
- 便携模式:插上 U 盘即用,配置跟着 U 盘走
|
||||
- 安装到电脑:一键永久安装
|
||||
- 支持远程控制(Agent 模式)
|
||||
- 基于 OpenClaw 开源项目
|
||||
|
||||
每日内容需要实用、具体、对真实用户有帮助。内容要中英双语,json格式,不要有多余的格式。`;
|
||||
|
||||
const USER_PROMPT = `请生成今天(${TODAY})的内容,包含两部分:
|
||||
|
||||
1. **每日小技巧**(tip):一个关于 U-Claw / OpenClaw 的具体使用技巧,可以是快捷操作、配置方法、隐藏功能等
|
||||
2. **每日最佳案例**(case):一个真实使用场景的案例,展示如何用 U-Claw 解决实际问题
|
||||
|
||||
要求:
|
||||
- 每部分有标题(title)和正文(content),正文 80-150 字
|
||||
- 中英双语:zh(中文)和 en(英文)版本
|
||||
- 实用、具体,避免空泛
|
||||
|
||||
请严格按以下 JSON 格式返回,不要有多余文字:
|
||||
{
|
||||
"tip": {
|
||||
"title": { "zh": "中文标题", "en": "English Title" },
|
||||
"content": { "zh": "中文内容", "en": "English content" },
|
||||
"tag": { "zh": "技巧", "en": "Tip" }
|
||||
},
|
||||
"case": {
|
||||
"title": { "zh": "中文标题", "en": "English Title" },
|
||||
"content": { "zh": "中文内容", "en": "English content" },
|
||||
"tag": { "zh": "案例", "en": "Case" }
|
||||
}
|
||||
}`;
|
||||
|
||||
function callClaude(apiKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify({
|
||||
model: 'claude-sonnet-4-6',
|
||||
max_tokens: 1024,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: USER_PROMPT }]
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.anthropic.com',
|
||||
path: '/v1/messages',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`API error ${res.statusCode}: ${data}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const text = parsed.content[0].text.trim();
|
||||
// 提取 JSON(防止模型输出额外文字)
|
||||
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) throw new Error('No JSON found in response');
|
||||
resolve(JSON.parse(jsonMatch[0]));
|
||||
} catch (e) {
|
||||
reject(new Error(`Parse error: ${e.message}\nRaw: ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('ANTHROPIC_API_KEY not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Generating content for ${TODAY}...`);
|
||||
|
||||
let content;
|
||||
try {
|
||||
content = await callClaude(apiKey);
|
||||
console.log('API call successful');
|
||||
} catch (err) {
|
||||
console.error('API call failed:', err.message);
|
||||
process.exit(0); // 失败时退出但不破坏 CI
|
||||
}
|
||||
|
||||
// 读取现有数据
|
||||
let data = { latest: null, history: [] };
|
||||
if (fs.existsSync(DATA_FILE)) {
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
||||
} catch (e) {
|
||||
console.warn('Could not parse existing data, starting fresh');
|
||||
}
|
||||
}
|
||||
|
||||
// 构建今日条目
|
||||
const entry = {
|
||||
date: TODAY,
|
||||
tip: content.tip,
|
||||
case: content.case
|
||||
};
|
||||
|
||||
// 如果今天已有记录,替换;否则追加到 history
|
||||
if (data.latest && data.latest.date !== TODAY) {
|
||||
data.history.unshift(data.latest);
|
||||
// 保留最近 MAX_HISTORY 条
|
||||
if (data.history.length > MAX_HISTORY) {
|
||||
data.history = data.history.slice(0, MAX_HISTORY);
|
||||
}
|
||||
}
|
||||
|
||||
data.latest = entry;
|
||||
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
|
||||
console.log(`Written to ${DATA_FILE}`);
|
||||
}
|
||||
|
||||
main();
|
||||
38
.github/workflows/daily-content.yml
vendored
Normal file
38
.github/workflows/daily-content.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Daily Content Generator
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # UTC 00:00 = 北京时间 08:00
|
||||
workflow_dispatch: # 支持手动触发测试
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Generate daily content
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
run: node .github/scripts/generate-daily.js
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add website/daily/data.json
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: update daily content $(date -u '+%Y-%m-%d')"
|
||||
git push
|
||||
fi
|
||||
22
website/daily/data.json
Normal file
22
website/daily/data.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"latest": {
|
||||
"date": "2026-04-02",
|
||||
"tip": {
|
||||
"title": { "zh": "便携模式:配置跟着 U 盘走", "en": "Portable Mode: Config Travels with Your USB" },
|
||||
"content": {
|
||||
"zh": "将 U-Claw 以便携模式运行时,所有配置、技能、模型 API Key 都保存在 U 盘本身,而非电脑上。换一台电脑插上 U 盘,立刻恢复完整工作环境。特别适合需要在多台电脑之间切换的开发者——出差时带上 U 盘,到哪里都是你熟悉的 AI 助手。",
|
||||
"en": "Running U-Claw in portable mode saves all configs, skills, and API keys on the USB drive itself, not the host computer. Plug into any machine and instantly restore your full environment — perfect for developers who work across multiple PCs or need a consistent AI setup on the go."
|
||||
},
|
||||
"tag": { "zh": "技巧", "en": "Tip" }
|
||||
},
|
||||
"case": {
|
||||
"title": { "zh": "用 U-Claw 一键审查 PR 代码", "en": "One-Click PR Code Review with U-Claw" },
|
||||
"content": {
|
||||
"zh": "某团队将 U-Claw 接入企业微信,配置「代码审查」技能。开发者提交 PR 后,直接将 diff 发给 AI,几秒钟内收到详细的审查意见:潜在 Bug、性能问题、代码风格建议一应俱全。审查效率提升 60%,同时覆盖了以往人工容易遗漏的边界条件。",
|
||||
"en": "A dev team connected U-Claw to their enterprise WeChat and configured the 'Code Review' skill. Developers paste PR diffs directly to the AI and get detailed feedback in seconds — bugs, performance issues, and style suggestions included. Review efficiency improved 60%, with better coverage of edge cases that manual review often misses."
|
||||
},
|
||||
"tag": { "zh": "案例", "en": "Case" }
|
||||
}
|
||||
},
|
||||
"history": []
|
||||
}
|
||||
@@ -309,6 +309,61 @@
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* Daily section */
|
||||
.daily-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.daily-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 28px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.daily-card:hover { border-color: var(--accent); }
|
||||
.daily-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.daily-date {
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.daily-history {
|
||||
margin-top: 32px;
|
||||
}
|
||||
.daily-history-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.daily-history-toggle:hover { border-color: var(--accent); color: var(--text); }
|
||||
.daily-history-list { display: none; margin-top: 20px; }
|
||||
.daily-history-list.open { display: block; }
|
||||
.daily-history-item {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 20px 0;
|
||||
}
|
||||
.daily-history-item-date {
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats { gap: 24px; }
|
||||
.hero-cta { flex-direction: column; align-items: center; }
|
||||
@@ -642,6 +697,63 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Daily Tip & Case -->
|
||||
<section id="daily" style="border-top: 1px solid var(--border); padding-top: 80px;">
|
||||
<h2>
|
||||
<span class="zh">每日精选</span>
|
||||
<span class="en hidden">Daily Picks</span>
|
||||
</h2>
|
||||
<p class="section-sub">
|
||||
<span class="zh">每天更新 · OpenClaw / U-Claw 使用技巧与最佳案例</span>
|
||||
<span class="en hidden">Updated daily · Tips & best-practice cases for OpenClaw / U-Claw</span>
|
||||
</p>
|
||||
|
||||
<div id="daily-date" class="daily-date"></div>
|
||||
|
||||
<div id="daily-loading" style="text-align:center;color:var(--text-dim);padding:40px 0;">
|
||||
<span class="zh">加载中…</span><span class="en hidden">Loading…</span>
|
||||
</div>
|
||||
|
||||
<div id="daily-content" style="display:none;">
|
||||
<div class="daily-grid">
|
||||
<!-- Tip card -->
|
||||
<div class="daily-card">
|
||||
<div class="daily-card-header">
|
||||
<span class="tag tag-blue" style="margin-bottom:0;">
|
||||
<span class="zh">技巧</span><span class="en hidden">Tip</span>
|
||||
</span>
|
||||
</div>
|
||||
<h3 id="tip-title" style="font-size:18px;margin-bottom:10px;"></h3>
|
||||
<p id="tip-content" style="color:var(--text-dim);font-size:14px;line-height:1.8;"></p>
|
||||
</div>
|
||||
<!-- Case card -->
|
||||
<div class="daily-card">
|
||||
<div class="daily-card-header">
|
||||
<span class="tag tag-green" style="margin-bottom:0;">
|
||||
<span class="zh">案例</span><span class="en hidden">Case</span>
|
||||
</span>
|
||||
</div>
|
||||
<h3 id="case-title" style="font-size:18px;margin-bottom:10px;"></h3>
|
||||
<p id="case-content" style="color:var(--text-dim);font-size:14px;line-height:1.8;"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History -->
|
||||
<div class="daily-history">
|
||||
<button class="daily-history-toggle" onclick="toggleDailyHistory(this)">
|
||||
<span class="zh">查看最近 7 天历史 ▼</span>
|
||||
<span class="en hidden">Recent 7 days history ▼</span>
|
||||
</button>
|
||||
<div class="daily-history-list" id="daily-history-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="daily-error" style="display:none;text-align:center;color:var(--text-dim);padding:40px 0;">
|
||||
<span class="zh">内容暂时无法加载,请稍后刷新</span>
|
||||
<span class="en hidden">Content unavailable, please refresh later</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="cta-section">
|
||||
<h2>
|
||||
@@ -774,7 +886,81 @@ function switchLang(lang) {
|
||||
document.getElementById('btn-zh').classList.toggle('active', lang === 'zh');
|
||||
document.getElementById('btn-en').classList.toggle('active', lang === 'en');
|
||||
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
||||
// Re-render daily content text after lang switch
|
||||
if (window._dailyData) renderDaily(window._dailyData);
|
||||
}
|
||||
|
||||
function getCurrentLang() {
|
||||
return document.getElementById('btn-zh').classList.contains('active') ? 'zh' : 'en';
|
||||
}
|
||||
|
||||
function t(obj) {
|
||||
const lang = getCurrentLang();
|
||||
return obj ? (obj[lang] || obj.zh || '') : '';
|
||||
}
|
||||
|
||||
function renderDaily(data) {
|
||||
const lang = getCurrentLang();
|
||||
const latest = data.latest;
|
||||
if (!latest) return;
|
||||
|
||||
document.getElementById('daily-date').textContent =
|
||||
lang === 'zh' ? `更新日期:${latest.date}` : `Updated: ${latest.date}`;
|
||||
|
||||
document.getElementById('tip-title').textContent = t(latest.tip.title);
|
||||
document.getElementById('tip-content').textContent = t(latest.tip.content);
|
||||
document.getElementById('case-title').textContent = t(latest.case.title);
|
||||
document.getElementById('case-content').textContent = t(latest.case.content);
|
||||
|
||||
// History (up to 7 entries)
|
||||
const histList = document.getElementById('daily-history-list');
|
||||
const recent = (data.history || []).slice(0, 7);
|
||||
histList.innerHTML = recent.map(entry => `
|
||||
<div class="daily-history-item">
|
||||
<div class="daily-history-item-date">${entry.date}</div>
|
||||
<div class="daily-grid" style="margin-bottom:0;">
|
||||
<div class="daily-card">
|
||||
<div class="daily-card-header">
|
||||
<span class="tag tag-blue" style="margin-bottom:0;">${t(entry.tip.tag)}</span>
|
||||
</div>
|
||||
<h3 style="font-size:16px;margin-bottom:8px;">${t(entry.tip.title)}</h3>
|
||||
<p style="color:var(--text-dim);font-size:13px;line-height:1.8;">${t(entry.tip.content)}</p>
|
||||
</div>
|
||||
<div class="daily-card">
|
||||
<div class="daily-card-header">
|
||||
<span class="tag tag-green" style="margin-bottom:0;">${t(entry.case.tag)}</span>
|
||||
</div>
|
||||
<h3 style="font-size:16px;margin-bottom:8px;">${t(entry.case.title)}</h3>
|
||||
<p style="color:var(--text-dim);font-size:13px;line-height:1.8;">${t(entry.case.content)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function toggleDailyHistory(btn) {
|
||||
const list = document.getElementById('daily-history-list');
|
||||
const open = list.classList.toggle('open');
|
||||
const lang = getCurrentLang();
|
||||
if (lang === 'zh') {
|
||||
btn.querySelector('.zh').textContent = open ? '收起历史 ▲' : '查看最近 7 天历史 ▼';
|
||||
} else {
|
||||
btn.querySelector('.en').textContent = open ? 'Collapse history ▲' : 'Recent 7 days history ▼';
|
||||
}
|
||||
}
|
||||
|
||||
// Load daily data
|
||||
fetch('daily/data.json?v=' + Date.now())
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
window._dailyData = data;
|
||||
document.getElementById('daily-loading').style.display = 'none';
|
||||
document.getElementById('daily-content').style.display = 'block';
|
||||
renderDaily(data);
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('daily-loading').style.display = 'none';
|
||||
document.getElementById('daily-error').style.display = 'block';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user