
News Summary
- 535 installs
- 2 repo stars
- Updated February 10, 2026
- zjfls/zhoujie-claude-skills
news-summary is a Claude Code skill that automatically fetches, categorizes, analyzes, and generates clean HTML summaries from the latest news on any developer-specified topic.
About
news-summary is a Node.js skill that fetches latest news for a given topic, categorizes and analyzes articles, then writes clean HTML summary pages into timestamped output directories. Each run creates a folder named with an ISO timestamp and topic slug under output/, optionally including category navigation and a separate analysis subdirectory. Developers reach for news-summary when they need repeatable topic digests—AI research roundups, competitive news watches, or internal briefing pages—without manually curating RSS feeds or formatting HTML reports.
- Generates timestamped output directories with data.json and summary.html
- Supports both simple and categorized HTML output modes
- Creates dedicated analysis subfolders for deeper inspection
- Runs as a reusable Node.js module for agentic news workflows
- Preserves full original data while producing readable summaries
News Summary by the numbers
- 535 all-time installs (skills.sh)
- Ranked #1,695 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zjfls/zhoujie-claude-skills --skill news-summaryAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 535 |
|---|---|
| repo stars | ★ 2 |
| Security audit | 1 / 3 scanners passed |
| Last updated | February 10, 2026 |
| Repository | zjfls/zhoujie-claude-skills ↗ |
How do you auto-generate HTML news digests by topic?
Automatically fetch, categorize, analyze and generate clean HTML summaries from latest news on any topic.
Who is it for?
Developers automating topic-based news digests who need categorized HTML reports saved to timestamped output folders.
Skip if: Real-time push alerting, paywalled source scraping without APIs, or long-form editorial writing beyond summarization.
When should I use this skill?
A developer asks to summarize latest news on a topic, generate a news HTML report, or build categorized topic digests.
What you get
Timestamped output directories with categorized HTML news summaries and optional analysis files per topic query.
- timestamped HTML news summaries
- categorized digest pages
- analysis subdirectory files
By the numbers
- Each query creates an independent timestamped directory under output/
- Supports optional categorized navigation via withCategories parameter
Files
新闻搜索摘要 Skill
一个智能新闻搜索和摘要工具,帮你快速获取和分析最新新闻。
核心功能
1. 搜索新闻
- 询问用户搜索主题
- 搜索最近 3-5 天的 12-25 条相关新闻
- 搜索工具:优先使用 WebSearch,不可用时使用 Brave Search MCP
- Brave Search 限制:每次调用后必须
sleep 1秒,防止速率限制 - 搜索优化:
- 使用多个关键词组合搜索
- 分批搜索确保足够结果
- 自动去重(标题/URL)
- 如结果不足 12 条,扩大时间范围或关键词
2. 分析新闻
对每条新闻提取:
- 标题
- 来源网站
- 发布时间
- 权威性评估(高/中/低)
- 摘要(100-200字)
- 原文链接
3. 生成 HTML 页面
- 输出目录:
<当前工作目录>/news-summary/<timestamp>_<topic>/ - 通过
pwd获取当前工作目录的绝对路径 <timestamp>格式:YYYYMMDD_HHMM(如:20260111_1145),确保每次搜索都有唯一目录<topic>从用户查询提取关键词(如:AI、deepseek),无法提取时使用news- 示例:
/path/to/work/news-summary/20260111_1145_AI/ - 文件名:
news_summary_<topic>.html - AI 解读目录:
<当前工作目录>/news-summary/<timestamp>_<topic>/analysis/ - 解读文件名:
news_analysis_<newsId>.md - 页面特性:
- 美观的响应式设计
- 卡片式新闻展示
- 每条新闻包含:标题、来源、时间、权威性标签、摘要
- 两个按钮:"查看原文" 和 "AI解读"
- 必须在 </body> 前包含:
<script src="/news-ai.js"></script>
4. AI 解读功能
- 服务器:Node.js HTTP 服务器(lib/server.js,端口 3456)
- 真实 AI 分析:通过 Claude Code CLI 生成深度解读
- 输出格式:完整的 HTML 页面
- 存储位置:
<工作目录>/news-summary/<timestamp_topic>/analysis/news_analysis_<id>.html - 包含完整的 HTML 结构(<!DOCTYPE html>、<html>、<head>、<body>等)
- 使用现代化的 CSS 样式,响应式设计
- 颜色主题使用 #667eea 和 #764ba2 渐变
- 包含新闻信息、分析内容、底部版权等完整结构
- 自定义 Prompt:点击"AI解读"按钮后可输入自定义分析角度
- 阻塞和超时:
- 生成时显示加载模态框
- 120 秒超时自动中止
- 文件管理:
- 已生成:显示"查看AI解读"和"删除解读"按钮
- 未生成:显示"AI解读"按钮
- 支持删除和重新生成
- 服务端点:
GET /check-analysis?newsId=<id>×tamp=<timestamp_topic>- 检查解读文件是否存在POST /analyze- 生成 AI 解读(支持 customPrompt,输出完整 HTML)DELETE /delete-analysis?newsId=<id>×tamp=<timestamp_topic>- 删除解读GET /view-analysis?newsId=<id>×tamp=<timestamp_topic>- 查看解读(直接返回 HTML)GET /news-summary/<timestamp_topic>/<filename>- 静态文件服务GET /news-ai.js- 前端脚本
5. 重启服务器并打开浏览器
- 重启服务器步骤(每次生成后必须执行):
1. 检查端口 3456 是否被占用:
- Windows:
netstat -ano | findstr 3456 - macOS/Linux:
lsof -ti:3456或netstat -ano | grep 3456
2. 如果端口被占用,停止旧服务器:
- Windows:
taskkill /F /PID <进程ID> - macOS/Linux:
kill -9 <进程ID>
3. 重新启动服务器:
- Windows:
start /B node <skill目录>/lib/server.js - macOS/Linux:
node <skill目录>/lib/server.js &
4. 等待 2 秒确保服务器启动 5. 验证启动:访问 http://localhost:3456/news-ai.js 应该返回 JavaScript 代码
- 生成 HTML 后,通过 HTTP 打开浏览器
- 浏览器命令:
- Windows:
Start-Process "http://localhost:3456/news-summary/<timestamp_topic>/news_summary_<topic>.html" - macOS:
open "http://localhost:3456/news-summary/<timestamp_topic>/news_summary_<topic>.html" - Linux:
xdg-open "http://localhost:3456/news-summary/<timestamp_topic>/news_summary_<topic>.html" - 重要:必须通过 HTTP 访问,不能使用 file:// 协议
工作流程
1. 询问用户搜索主题 2. 生成唯一标识:
- 从查询中提取关键词作为
<topic>(无法提取则用news) - 生成时间戳:
<timestamp>=YYYYMMDD_HHMM格式 - 组合为:
<timestamp>_<topic>(如:20260111_1145_AI)
3. 搜索新闻(WebSearch 优先;Brave Search 需间隔 1 秒) 4. 分析和整理新闻信息 5. 获取当前工作目录:使用 Bash 命令 pwd 获取绝对路径 6. 创建目录:<当前工作目录>/news-summary/<timestamp>_<topic>/ 7. 生成 HTML:
- 文件路径:
<当前工作目录>/news-summary/<timestamp>_<topic>/news_summary_<topic>.html - 必须在 </body> 前添加:
<script src="/news-ai.js"></script> - 每个新闻卡片必须包含 data 属性(data-news-id, data-news-url, data-news-source, data-news-time)
8. 重启服务器:
- 检查端口 3456 是否被占用
- 如果被占用,停止旧服务器
- 启动新服务器:
node <skill目录>/lib/server.js & - 等待 2 秒并验证
9. 打开浏览器:http://localhost:3456/news-summary/<timestamp>_<topic>/news_summary_<topic>.html
HTML 模板要求
- 现代简洁设计,响应式布局
- 卡片式新闻展示
- 每个新闻卡片必须包含 data 属性:
data-news-id: 新闻 ID(0, 1, 2...)data-news-url: 原文链接data-news-source: 来源data-news-time: 发布时间data-news-title: 新闻标题data-news-summary: 新闻摘要- 权威性标签:高(绿色)、中(黄色)、低(灰色)
- 相对时间显示(如:2天前)
- 必须在 </body> 前引入:
<script src="/news-ai.js"></script>(使用绝对路径) - 初始只包含"查看原文"按钮,AI 解读按钮由 news-ai.js 在页面加载后动态添加
注意事项
- 使用绝对路径(通过
pwd获取当前工作目录) - 创建必要的目录结构
- 处理网络请求失败
- 验证新闻来源可靠性
- 必须在 HTML 中包含
<script src="/news-ai.js"></script>,否则 AI 解读功能不可用 - 时间戳格式:必须使用
YYYYMMDD_HHMM格式,确保每次搜索都有唯一目录 - 服务器必须重启:每次生成新闻后必须重启服务器,确保 WORK_DIR 指向当前工作目录
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
/**
* 生成新闻摘要HTML,每次查询创建独立的时间戳目录
* @param {string} topicName - 查询主题名称(如:'AI技术新闻'、'物理引擎技术')
* @param {Array} newsData - 新闻数据数组
* @param {boolean} withCategories - 是否包含分类导航
*/
function generateNewsHtml(topicName, newsData, withCategories = false) {
// 生成时间戳目录名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19).replace('T', '_');
const dirName = `${timestamp}_${topicName}`;
// 创建目录结构
const baseDir = path.join(__dirname, 'output');
const queryDir = path.join(baseDir, dirName);
const analysisDir = path.join(queryDir, 'analysis');
// 确保目录存在
[baseDir, queryDir, analysisDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// 保存原始数据
const dataPath = path.join(queryDir, 'data.json');
fs.writeFileSync(dataPath, JSON.stringify(newsData, null, 2), 'utf8');
// 生成HTML文件
const htmlPath = path.join(queryDir, 'summary.html');
let html;
if (withCategories && newsData.some(item => item.category)) {
html = generateCategorizedHtml(newsData, topicName, dirName);
} else {
html = generateSimpleHtml(newsData, topicName, dirName);
}
fs.writeFileSync(htmlPath, html, 'utf8');
console.log(`✓ 新闻摘要已生成`);
console.log(` 目录: output/${dirName}/`);
console.log(` 文件: summary.html, data.json`);
return { htmlPath, queryDir, dirName };
}
function generateCategorizedHtml(newsData, topicName, dirName) {
// 按分类组织新闻
const categories = {};
newsData.forEach(item => {
const cat = item.category || '未分类';
if (!categories[cat]) {
categories[cat] = [];
}
categories[cat].push(item);
});
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${topicName} - 新闻摘要</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.header {
max-width: 1400px;
margin: 0 auto 30px;
text-align: center;
color: white;
}
.header h1 {
font-size: 36px;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 16px;
opacity: 0.9;
}
.stats {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
font-size: 14px;
flex-wrap: wrap;
}
.badge {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
}
.main-container {
max-width: 1400px;
margin: 0 auto;
display: flex;
gap: 20px;
}
.sidebar {
width: 250px;
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
position: sticky;
top: 20px;
height: fit-content;
}
.sidebar h2 {
font-size: 18px;
margin-bottom: 15px;
color: #1a202c;
}
.category-nav {
list-style: none;
}
.category-item {
margin-bottom: 8px;
}
.category-link {
display: block;
padding: 8px 12px;
color: #4a5568;
text-decoration: none;
border-radius: 6px;
transition: all 0.2s ease;
font-size: 14px;
}
.category-link:hover {
background: #f7fafc;
color: #667eea;
}
.category-count {
float: right;
background: #e2e8f0;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
}
.content {
flex: 1;
}
.category-section {
margin-bottom: 40px;
}
.category-header {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.category-header h2 {
font-size: 24px;
color: #1a202c;
margin-bottom: 5px;
}
.category-header p {
color: #718096;
font-size: 14px;
}
.news-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px;
}
.news-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.news-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
}
.news-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
}
.news-title {
font-size: 17px;
font-weight: bold;
color: #1a202c;
margin-bottom: 12px;
line-height: 1.4;
}
.news-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 12px;
font-size: 13px;
}
.source, .time {
color: #4a5568;
display: flex;
align-items: center;
gap: 4px;
}
.authority {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.authority.high { background: #10b981; color: white; }
.authority.medium { background: #f59e0b; color: white; }
.authority.low { background: #9ca3af; color: white; }
.news-summary {
flex: 1;
color: #4a5568;
line-height: 1.7;
margin-bottom: 16px;
font-size: 14px;
}
.news-actions {
display: flex;
gap: 10px;
margin-top: auto;
}
.btn {
flex: 1;
padding: 10px 16px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
text-align: center;
display: inline-block;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #764ba2;
color: white;
}
.btn-secondary:hover {
background: #6a4293;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(118, 75, 162, 0.3);
}
.footer {
max-width: 1400px;
margin: 30px auto 0;
text-align: center;
color: white;
font-size: 14px;
opacity: 0.8;
}
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.85);
color: white;
padding: 24px 48px;
border-radius: 12px;
display: none;
z-index: 1000;
font-size: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.loading.active { display: block; }
@media (max-width: 1200px) {
.main-container { flex-direction: column; }
.sidebar { width: 100%; position: relative; }
.news-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<h1>${topicName}</h1>
<p>新闻摘要 - 最近3-5天内的技术突破与行业动态</p>
<div class="stats">
<span class="badge">共 ${newsData.length} 条技术新闻</span>
<span class="badge">${Object.keys(categories).length} 个分类</span>
<span class="badge">查询目录: ${dirName}</span>
</div>
</div>
<div class="main-container">
<aside class="sidebar">
<h2>目录导航</h2>
<ul class="category-nav">
${Object.keys(categories).map(category => `
<li class="category-item">
<a href="#${encodeURIComponent(category)}" class="category-link">
${category}
<span class="category-count">${categories[category].length}</span>
</a>
</li>
`).join('')}
</ul>
</aside>
<main class="content">
${Object.keys(categories).map(category => `
<section id="${encodeURIComponent(category)}" class="category-section">
<div class="category-header">
<h2>${category}</h2>
<p>${categories[category].length} 条相关新闻</p>
</div>
<div class="news-grid">
${categories[category].map((item, index) => generateNewsCard(item, dirName)).join('')}
</div>
</section>
`).join('')}
</main>
</div>
<div class="footer">
<p>由 Claude Code 生成 | 查询目录: output/${dirName}/ | AI解读: analysis/</p>
</div>
<div class="loading" id="loading">正在生成AI解读...</div>
<script>
${getClientScript(dirName)}
</script>
</body>
</html>`;
}
function generateSimpleHtml(newsData, topicName, dirName) {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${topicName} - 新闻摘要</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.header {
max-width: 1200px;
margin: 0 auto 30px;
text-align: center;
color: white;
}
.header h1 { font-size: 36px; margin-bottom: 10px; }
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.news-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.news-title {
font-size: 18px;
font-weight: bold;
color: #1a202c;
margin-bottom: 12px;
}
.news-actions {
display: flex;
gap: 10px;
margin-top: 16px;
}
.btn {
flex: 1;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
text-align: center;
text-decoration: none;
cursor: pointer;
}
.btn-primary { background: #667eea; color: white; }
.btn-secondary { background: #764ba2; color: white; }
</style>
</head>
<body>
<div class="header">
<h1>${topicName}</h1>
<p>共 ${newsData.length} 条新闻 | ${dirName}</p>
</div>
<div class="container">
${newsData.map(item => generateNewsCard(item, dirName)).join('')}
</div>
<script>${getClientScript(dirName)}</script>
</body>
</html>`;
}
function generateNewsCard(item, dirName) {
const authority = item.authority || 'medium';
const authorityText = {
'high': '权威来源',
'medium': '一般来源',
'low': '待验证'
}[authority.toLowerCase()] || '未知';
return `
<div class="news-card">
<div class="news-title">${item.title}</div>
<div class="news-meta">
<span class="source">📰 ${item.source}</span>
<span class="time">⏰ ${item.time}</span>
<span class="authority ${authority.toLowerCase()}">${authorityText}</span>
</div>
<div class="news-summary">${item.summary}</div>
<div class="news-actions">
<a href="${item.url}" target="_blank" class="btn btn-primary">查看原文</a>
<button class="btn btn-secondary" onclick="analyzeNews('${dirName}', '${item.title.replace(/'/g, "\\'")}', '${item.url}', \`${item.summary.replace(/`/g, '\\`')}\`, '${item.source}', '${item.time}')">AI解读</button>
</div>
</div>`;
}
function getClientScript(dirName) {
return `
async function analyzeNews(dirName, title, url, content, source, time) {
const loading = document.getElementById('loading');
if (loading) loading.classList.add('active');
try {
const response = await fetch('http://localhost:3456/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dirName, title, url, content, source, time })
});
const result = await response.json();
if (result.success) {
console.log('AI解读已生成:', result.filePath);
} else {
alert('生成AI解读失败: ' + result.error);
}
} catch (error) {
alert('无法连接到AI解读服务器。请确保服务器正在运行。\\n错误: ' + error.message);
} finally {
if (loading) setTimeout(() => loading.classList.remove('active'), 500);
}
}
// 平滑滚动
document.querySelectorAll('.category-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
`;
}
// 打开浏览器
function openInBrowser(htmlPath) {
// 优先使用PowerShell(Windows上更可靠)
if (process.platform === 'win32') {
exec(`powershell -Command "Start-Process '${htmlPath}'"`, (error) => {
if (error) {
console.error('PowerShell打开失败,尝试使用start命令');
exec(`start "" "${htmlPath}"`, (error2) => {
if (error2) {
console.error('打开浏览器失败:', error2);
console.log('请手动打开:', htmlPath);
} else {
console.log('浏览器已打开');
}
});
} else {
console.log('浏览器已打开(PowerShell)');
}
});
} else {
const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
exec(`${command} "${htmlPath}"`, (error) => {
if (error) {
console.error('打开浏览器失败:', error);
console.log('请手动打开:', htmlPath);
} else {
console.log('浏览器已打开');
}
});
}
}
module.exports = { generateNewsHtml, openInBrowser };
const fs = require('fs');
const path = require('path');
/**
* 生成新闻摘要HTML的通用函数
* @param {Object} options - 配置选项
* @param {string} options.topic - 主题标识(如: 'ai-news', 'physics-engine')
* @param {string} options.topicName - 主题显示名称(如: 'AI行业技术', '物理引擎技术')
* @param {string} options.dataFile - 数据文件名(在output/data/目录下)
* @param {boolean} options.withCategories - 是否包含分类导航
*/
function generateNewsHtml(options) {
const { topic, topicName, dataFile, withCategories = false } = options;
// 创建目录结构
const baseDir = path.join(__dirname, 'output');
const topicDir = path.join(baseDir, topic);
const dataDir = path.join(baseDir, 'data');
// 确保目录存在
[baseDir, topicDir, dataDir, path.join(baseDir, 'analysis')].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// 读取新闻数据
const dataPath = path.join(dataDir, dataFile);
if (!fs.existsSync(dataPath)) {
console.error(`数据文件不存在: ${dataPath}`);
process.exit(1);
}
const newsData = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
// 生成HTML文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
const outputPath = path.join(topicDir, `${topic}_${timestamp}.html`);
let html;
if (withCategories) {
html = generateCategorizedHtml(newsData, topicName, topic);
} else {
html = generateSimpleHtml(newsData, topicName, topic);
}
fs.writeFileSync(outputPath, html, 'utf8');
console.log(`✓ HTML文件已生成: ${outputPath}`);
console.log(`✓ 目录: output/${topic}/`);
return outputPath;
}
function generateSimpleHtml(newsData, topicName, topic) {
// ... (简单HTML模板,不带分类)
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${topicName}新闻摘要</title>
<style>
/* ... CSS样式 ... */
</style>
</head>
<body>
<div class="header">
<h1>${topicName}新闻摘要</h1>
<p>最近3-5天内的技术突破与行业动态</p>
</div>
<div class="container">
${newsData.map((item, index) => generateNewsCard(item, index, topic)).join('')}
</div>
</body>
</html>`;
}
function generateCategorizedHtml(newsData, topicName, topic) {
// 按分类组织新闻
const categories = {};
newsData.forEach(item => {
if (!categories[item.category]) {
categories[item.category] = [];
}
categories[item.category].push(item);
});
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${topicName}新闻摘要</title>
<style>
/* ... 完整CSS样式 ... */
</style>
</head>
<body>
<div class="header">
<h1>${topicName}新闻摘要</h1>
<p>最近3-5天内的技术突破与行业动态</p>
<div class="stats">
<span class="badge">共 ${newsData.length} 条技术新闻</span>
<span class="badge">${Object.keys(categories).length} 个分类</span>
</div>
</div>
<div class="main-container">
<aside class="sidebar">
<h2>📑 目录导航</h2>
<ul class="category-nav">
${Object.keys(categories).map(category => `
<li class="category-item">
<a href="#${encodeURIComponent(category)}" class="category-link">
${category}
<span class="category-count">${categories[category].length}</span>
</a>
</li>
`).join('')}
</ul>
</aside>
<main class="content">
${Object.keys(categories).map(category => `
<section id="${encodeURIComponent(category)}" class="category-section">
<div class="category-header">
<h2>${category}</h2>
<p>${categories[category].length} 条相关新闻</p>
</div>
<div class="news-grid">
${categories[category].map((item, index) => generateNewsCard(item, index, topic)).join('')}
</div>
</section>
`).join('')}
</main>
</div>
<div class="footer">
<p>🤖 由 Claude Code 生成 | 目录: output/${topic}/ | AI解读: output/analysis/</p>
</div>
</body>
</html>`;
}
function generateNewsCard(item, index, topic) {
return `
<div class="news-card">
<div class="news-title">${item.title}</div>
<div class="news-meta">
<span class="source">📰 ${item.source}</span>
<span class="time">⏰ ${item.time}</span>
<span class="authority ${item.authority.toLowerCase()}">${getAuthorityText(item.authority)}</span>
</div>
<div class="news-summary">${item.summary}</div>
<div class="news-actions">
<a href="${item.url}" target="_blank" class="btn btn-primary">查看原文</a>
<button class="btn btn-secondary" onclick="analyzeNews('${topic}', '${item.title.replace(/'/g, "\\'")}', '${item.url}', \`${item.summary.replace(/`/g, '\\`')}\`, '${item.source}', '${item.time}')">AI解读</button>
</div>
</div>`;
}
function getAuthorityText(authority) {
const authorityMap = {
'high': '权威来源',
'medium': '一般来源',
'low': '待验证'
};
return authorityMap[authority.toLowerCase()] || '未知';
}
module.exports = { generateNewsHtml };
(function() {
'use strict';
function getTimestamp() {
const pathParts = window.location.pathname.split('/');
return pathParts[2] || '';
}
async function checkAnalysis(newsId, timestamp) {
try {
const response = await fetch(`/check-analysis?newsId=${newsId}×tamp=${timestamp}`);
const data = await response.json();
return data.exists;
} catch (error) {
console.error('Error checking analysis:', error);
return false;
}
}
function viewAnalysis(newsId, timestamp) {
window.open(`/view-analysis?newsId=${newsId}×tamp=${timestamp}`, '_blank');
}
async function deleteAnalysis(newsId, timestamp) {
if (!confirm('确定要删除这条AI解读吗?删除后可以重新生成。')) {
return false;
}
try {
const response = await fetch(`/delete-analysis?newsId=${newsId}×tamp=${timestamp}`, {
method: 'DELETE'
});
const data = await response.json();
return data.success;
} catch (error) {
console.error('Error deleting analysis:', error);
alert('删除失败:' + error.message);
return false;
}
}
async function generateAnalysis(newsData, customPrompt = '') {
const { newsId, newsUrl, newsSource, newsTime, title, summary, timestamp } = newsData;
try {
const response = await fetch('/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
newsId,
newsUrl,
newsSource,
newsTime,
timestamp,
title,
summary,
customPrompt
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '生成失败');
}
const result = await response.json();
return { success: true, message: result.message };
} catch (error) {
return { success: false, error: error.message };
}
}
function showLoadingModal(message = '正在生成AI解读...') {
const existingModal = document.getElementById('loading-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.id = 'loading-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 12px; text-align: center; max-width: 400px; width: 90%;">
<div class="spinner" style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"></div>
<p style="color: #333; font-size: 16px; margin: 0;">${message}</p>
<p id="elapsed-time" style="color: #667eea; font-size: 14px; margin-top: 10px; font-weight: 600;">已用时:0秒</p>
<div style="width: 100%; height: 8px; background: #f3f3f3; border-radius: 4px; margin-top: 15px; overflow: hidden;">
<div id="progress-bar" style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; width: 0%; transition: width 0.3s ease;"></div>
</div>
</div>
`;
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
document.body.appendChild(modal);
// 启动计时器和进度条更新
const startTime = Date.now();
const updateInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const timeEl = document.getElementById('elapsed-time');
const progressBar = document.getElementById('progress-bar');
if (timeEl) {
timeEl.textContent = `已用时:${elapsed}秒`;
}
if (progressBar) {
// 假进度条:快速到30%,然后逐渐减缓,最多到95%
let progress;
if (elapsed < 5) {
progress = elapsed * 6; // 0-30%
} else if (elapsed < 30) {
progress = 30 + (elapsed - 5) * 2; // 30-80%
} else {
progress = 80 + Math.min(15, (elapsed - 30) * 0.5); // 80-95%
}
progressBar.style.width = `${Math.min(95, progress)}%`;
}
}, 1000);
// 保存计时器ID,方便清除
modal.dataset.intervalId = updateInterval;
}
function hideLoadingModal() {
const modal = document.getElementById('loading-modal');
if (modal) {
// 清除定时器
const intervalId = modal.dataset.intervalId;
if (intervalId) {
clearInterval(Number(intervalId));
}
modal.remove();
}
}
function showCustomPromptModal(newsData, onSubmit) {
const existingModal = document.getElementById('custom-prompt-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.id = 'custom-prompt-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 12px; max-width: 600px; width: 90%;">
<h3 style="margin: 0 0 20px 0; color: #333;">自定义AI解读角度</h3>
<p style="color: #666; margin-bottom: 15px;">可以指定特定的分析角度,例如:技术影响、商业价值、用户体验等(留空使用默认分析)</p>
<textarea id="custom-prompt-input"
placeholder="例如:请从技术架构的角度分析这个新闻..."
style="width: 100%; min-height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; font-family: inherit; resize: vertical;"
></textarea>
<div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancel-btn" style="padding: 10px 20px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: pointer;">取消</button>
<button id="submit-btn" style="padding: 10px 20px; border: none; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-radius: 6px; cursor: pointer;">生成解读</button>
</div>
</div>
`;
document.body.appendChild(modal);
const textarea = modal.querySelector('#custom-prompt-input');
const cancelBtn = modal.querySelector('#cancel-btn');
const submitBtn = modal.querySelector('#submit-btn');
cancelBtn.onclick = () => modal.remove();
submitBtn.onclick = () => {
const customPrompt = textarea.value.trim();
modal.remove();
onSubmit(customPrompt);
};
// 点击背景关闭
modal.onclick = (e) => {
if (e.target === modal) {
modal.remove();
}
};
// 聚焦输入框
setTimeout(() => textarea.focus(), 100);
}
async function handleAnalyzeClick(newsData) {
showCustomPromptModal(newsData, async (customPrompt) => {
showLoadingModal('正在生成AI解读...');
const startTime = Date.now();
const timeout = 120000; // 120秒超时
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时,AI解读生成时间过长')), timeout);
});
try {
const result = await Promise.race([
generateAnalysis(newsData, customPrompt),
timeoutPromise
]);
hideLoadingModal();
if (result.success) {
alert('AI解读生成成功!');
// 刷新按钮状态
location.reload();
} else {
alert('生成失败:' + result.error);
}
} catch (error) {
hideLoadingModal();
alert('生成失败:' + error.message);
}
});
}
async function updateButtonState(card, newsData, exists) {
const { newsId, timestamp } = newsData;
const buttonContainer = card.querySelector('.news-actions');
if (!buttonContainer) return;
// 清除现有按钮
const oldButtons = buttonContainer.querySelectorAll('.ai-analyze-btn, .delete-analysis-btn');
oldButtons.forEach(btn => btn.remove());
if (exists) {
// 已有解读:显示"查看AI解读"和"删除解读"按钮
const viewBtn = document.createElement('button');
viewBtn.textContent = '查看AI解读';
viewBtn.className = 'btn btn-success ai-analyze-btn';
viewBtn.onclick = () => viewAnalysis(newsId, timestamp);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除解读';
deleteBtn.className = 'btn btn-danger delete-analysis-btn';
deleteBtn.onclick = async () => {
const success = await deleteAnalysis(newsId, timestamp);
if (success) {
alert('解读已删除');
location.reload();
}
};
buttonContainer.appendChild(viewBtn);
buttonContainer.appendChild(deleteBtn);
} else {
// 未生成:显示"AI解读"按钮
const analyzeBtn = document.createElement('button');
analyzeBtn.textContent = 'AI解读';
analyzeBtn.className = 'btn btn-primary ai-analyze-btn';
analyzeBtn.onclick = () => handleAnalyzeClick(newsData);
buttonContainer.appendChild(analyzeBtn);
}
}
async function initializeButtons() {
const timestamp = getTimestamp();
const newsCards = document.querySelectorAll('.news-card');
for (const card of newsCards) {
const newsId = card.getAttribute('data-news-id');
const newsUrl = card.getAttribute('data-news-url');
const newsSource = card.getAttribute('data-news-source');
const newsTime = card.getAttribute('data-news-time');
const titleEl = card.querySelector('.news-title');
const summaryEl = card.querySelector('.news-summary');
const title = titleEl ? titleEl.textContent : '';
const summary = summaryEl ? summaryEl.textContent : '';
const newsData = {
newsId,
newsUrl,
newsSource,
newsTime,
title,
summary,
timestamp
};
const exists = await checkAnalysis(newsId, timestamp);
await updateButtonState(card, newsData, exists);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeButtons);
} else {
initializeButtons();
}
})();
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const { spawn } = require('child_process');
const PORT = 3456;
const BASE_DIR = path.join(__dirname, "..");
// 支持从环境变量或当前工作目录读取工作目录
const WORK_DIR = process.env.NEWS_WORK_DIR || process.cwd();
console.log(`Server BASE_DIR: ${BASE_DIR}`);
console.log(`Server WORK_DIR: ${WORK_DIR}`);
const ANALYSIS_TIMEOUT = 120000; // 120秒超时
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.md': 'text/markdown; charset=utf-8'
};
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
console.log(`${req.method} ${pathname}`);
if (pathname === '/news-ai.js') {
const scriptPath = path.join(__dirname, 'news-ai.js');
fs.readFile(scriptPath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('news-ai.js not found');
return;
}
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
res.end(data);
});
return;
}
if (pathname === '/check-analysis' && req.method === 'GET') {
const newsId = parsedUrl.query.newsId;
const timestamp = parsedUrl.query.timestamp;
if (!newsId || !timestamp) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing newsId or timestamp' }));
return;
}
const analysisFile = path.join(WORK_DIR, 'news-summary', timestamp, 'analysis', `news_analysis_${newsId}.html`);
fs.access(analysisFile, fs.constants.F_OK, (err) => {
console.log('发送成功响应'); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ exists: !err }));
});
return;
}
if (pathname === '/analyze' && req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
console.log('收到analyze请求');
try {
const data = JSON.parse(body);
const { newsId, newsUrl, newsSource, newsTime, timestamp, title, summary, customPrompt } = data;
const analysisDir = path.join(WORK_DIR, 'news-summary', timestamp, 'analysis');
const analysisFile = path.join(analysisDir, `news_analysis_${newsId}.html`);
// 构建AI提示词
const newsTitle = title || '未知标题';
const newsSummary = summary || '暂无摘要';
let aiPrompt = `你是一个HTML生成器。你的任务是生成一个完整的HTML页面。
重要:你的输出必须是纯HTML代码,从<!DOCTYPE html>开始,到</html>结束。不要包含任何解释性文字、说明或其他内容。
## 新闻信息
- **标题**: ${newsTitle}
- **来源**: ${newsSource || '未知来源'}
- **时间**: ${newsTime || '未知时间'}
- **链接**: ${newsUrl || '#'}
## 新闻摘要
${newsSummary}
## 分析要求
请对这条新闻进行深度分析,包括:
1. **背景分析**:事件的历史背景和相关上下文
2. **关键要点**:提炼出最重要的3-5个要点
3. **影响评估**:分析对行业、技术、用户等方面的影响
4. **相关延伸**:相关话题和深入思考的方向`;
if (customPrompt) {
aiPrompt += `\n\n## 用户自定义分析角度\n${customPrompt}`;
}
aiPrompt += `
## HTML页面要求
1. 使用现代化的CSS样式,美观易读
2. 响应式设计,适配移动端和桌面端
3. 使用渐变背景和卡片布局
4. 包含新闻信息、分析内容、底部版权等完整结构
5. 颜色主题使用 #667eea 和 #764ba2 渐变
6. 字体使用系统默认字体栈
## 输出格式
你必须只输出HTML代码,从<!DOCTYPE html>开始。
不要输出任何其他文字,包括:
- 不要说"我为您生成了..."
- 不要说"页面包含..."
- 不要说"如果您需要..."
- 只输出纯HTML代码,没有任何解释
立即开始输出HTML代码:`;
// 创建分析目录
if (!fs.existsSync(analysisDir)) {
fs.mkdirSync(analysisDir, { recursive: true });
}
// 调用Claude CLI生成AI解读
console.log('调用Claude CLI生成分析...');
const claudeProcess = spawn('claude', [
'--print', // 非交互式模式
'--model', 'kimi-k2-thinking-turbo' // 使用kimi模型
], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
let output = '';
let errorOutput = '';
let timeoutId;
let isTimeout = false;
// 设置超时
timeoutId = setTimeout(() => {
isTimeout = true;
claudeProcess.kill();
console.log('Claude CLI调用超时');
}, ANALYSIS_TIMEOUT);
claudeProcess.stdout.on('data', (data) => {
output += data.toString();
});
claudeProcess.stderr.on('data', (data) => {
errorOutput += data.toString();
console.error('Claude CLI错误输出:', data.toString());
});
claudeProcess.on('close', (code) => {
clearTimeout(timeoutId);
if (isTimeout) {
res.writeHead(408, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'AI分析请求超时,请稍后重试' }));
return;
}
if (code !== 0) {
console.error('Claude CLI执行失败,退出码:', code);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `AI分析生成失败: ${errorOutput}` }));
return;
}
// 提取 HTML 内容
let htmlContent = output.trim();
// 尝试提取 HTML 代码(如果 Claude 输出了额外的文字)
const htmlMatch = htmlContent.match(/<!DOCTYPE html>[\s\S]*<\/html>/i);
if (htmlMatch) {
htmlContent = htmlMatch[0];
console.log('成功提取HTML代码');
} else if (!htmlContent.startsWith('<!DOCTYPE')) {
// 如果没有找到完整的 HTML,但内容不是以 <!DOCTYPE 开头,记录警告
console.warn('警告:输出可能不是有效的HTML');
}
fs.writeFile(analysisFile, htmlContent, 'utf8', (err) => {
if (err) {
console.error('写入分析文件失败:', err);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '保存分析文件失败' }));
return;
}
console.log('分析生成成功');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: '分析生成成功' }));
});
});
// 发送提示词到Claude CLI
claudeProcess.stdin.write(aiPrompt);
claudeProcess.stdin.end();
} catch (err) {
console.error('处理请求失败:', err);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
return;
}
if (pathname === '/delete-analysis' && req.method === 'DELETE') {
const newsId = parsedUrl.query.newsId;
const timestamp = parsedUrl.query.timestamp;
if (!newsId || !timestamp) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing newsId or timestamp' }));
return;
}
const analysisFile = path.join(WORK_DIR, 'news-summary', timestamp, 'analysis', `news_analysis_${newsId}.html`);
fs.unlink(analysisFile, (err) => {
if (err) {
console.error('删除分析文件失败:', err);
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '分析文件不存在或删除失败' }));
return;
}
console.log('分析文件已删除:', analysisFile);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: '分析文件已删除' }));
});
return;
}
if (pathname === '/view-analysis' && req.method === 'GET') {
const newsId = parsedUrl.query.newsId;
const timestamp = parsedUrl.query.timestamp;
if (!newsId || !timestamp) {
res.writeHead(400);
res.end('Missing newsId or timestamp');
return;
}
const analysisFile = path.join(WORK_DIR, 'news-summary', timestamp, 'analysis', `news_analysis_${newsId}.html`);
fs.readFile(analysisFile, 'utf8', (err, data) => {
if (err) {
res.writeHead(404);
res.end('Analysis not found');
return;
}
// 直接返回 HTML 内容
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(data);
});
return;
}
// 支持两种路径:
// 1. /output/ - 从 skill 目录的 output 子目录(向后兼容)
// 2. /news-summary/ - 从当前工作目录的 news-summary 子目录
if (pathname.startsWith('/output/')) {
const filePath = path.join(BASE_DIR, pathname);
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('File not found');
return;
}
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
});
return;
}
if (pathname.startsWith('/news-summary/')) {
const filePath = path.join(WORK_DIR, pathname);
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('File not found');
return;
}
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
});
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
新闻搜索摘要 Skill
一个用于搜索最近3-5天内的新闻、生成HTML摘要页面并支持AI解读功能的Claude Code skill。
功能特性
- 🔍 智能搜索:搜索最近3-5天内的新闻内容
- 📊 权威评估:自动评估新闻来源的权威性(高/中/低)
- 🎨 美观展示:生成现代化、响应式的HTML页面
- 🤖 AI解读:点击按钮即可获取AI深度分析
- ⚡ 自动打开:生成后自动在浏览器中打开
- 🕒 时间标记:清晰显示新闻发布时间和来源信息
目录结构
.claude/skills/news-summary/
├── skill.json # Skill配置文件
├── index.md # Skill入口点(提示词)
├── server.js # AI解读后端服务器
├── lib/
│ └── html-generator.js # HTML生成工具库
├── package.json # Node.js项目配置
├── output/ # 生成的文件输出目录
│ ├── 2025-12-08_14-30-00_AI技术新闻/
│ │ ├── summary.html # 新闻摘要HTML
│ │ ├── data.json # 原始数据
│ │ └── analysis/ # AI解读文件
│ │ └── analysis_xxx.html
│ └── 2025-12-08_15-20-00_物理引擎/
│ ├── summary.html
│ ├── data.json
│ └── analysis/
└── README.md # 使用说明文档输出目录说明
每次查询会自动创建一个独立的目录,格式为:{时间戳}_{查询主题}/
例如:2025-12-08_14-30-00_AI技术新闻/
每个查询目录包含:
summary.html- 新闻摘要HTML页面data.json- 原始JSON数据analysis/- 该次查询的所有AI解读文件
使用方法
1. 启动AI解读服务器
在使用skill之前,需要先启动AI解读服务器:
cd ~/.claude/skills/news-summary
node server.js服务器将在 http://localhost:3456 上运行。
2. 使用Skill
在Claude Code中激活skill:
/skill news-summary然后按照提示操作:
1. 输入你想搜索的新闻主题或关键词 2. 等待Claude搜索并分析新闻 3. 生成的HTML页面会自动在浏览器中打开 4. 点击任意新闻卡片上的"AI解读"按钮获取深度分析
工作流程
用户输入主题
↓
使用WebSearch搜索新闻
↓
分析新闻信息
↓
生成HTML摘要页面
↓
自动打开浏览器
↓
用户点击"AI解读"按钮
↓
发送请求到本地服务器
↓
生成AI解读HTML页面
↓
自动打开新页面生成的HTML功能
新闻摘要页面
- 新闻卡片:每条新闻以卡片形式展示
- 元信息:显示来源、时间、权威性标签
- 操作按钮:
- "查看原文":跳转到新闻原始链接
- "AI解读":生成AI深度分析
AI解读页面
AI解读页面包含以下部分:
- 📋 新闻摘要:新闻内容概述
- 🔍 背景分析:事件的历史和背景信息
- 💡 关键要点:核心观点和主要影响
- 📊 影响评估:正面和负面影响分析
- 🔗 相关延伸:相关话题和深入思考
权威性评估标准
- 高(绿色):权威媒体、官方网站、知名新闻机构
- 中(黄色):一般媒体、行业网站、区域性媒体
- 低(灰色):个人博客、社交媒体、待验证来源
技术栈
- Claude Code:AI能力和自动化
- Node.js:后端服务器
- HTML/CSS/JavaScript:前端页面
- HTTP:本地服务器通信
配置说明
端口配置
默认端口:3456
如需修改端口,编辑 server.js 文件:
const PORT = 3456; // 修改为你想要的端口同时需要修改 template.js 中的端口:
const response = await fetch('http://localhost:3456/analyze', {
// 修改为相同的端口输出目录
默认输出目录:~/.claude/skills/news-summary/output/
如需修改,编辑 server.js 中的 OUTPUT_DIR 常量。
注意事项
1. 服务器运行:使用AI解读功能前,必须先启动server.js服务器 2. 浏览器支持:需要现代浏览器(支持ES6+和Fetch API) 3. 网络连接:需要网络连接以搜索新闻和访问原文 4. CORS:服务器已配置CORS,允许本地HTML文件访问 5. 文件管理:生成的HTML文件会保存在output目录,建议定期清理 6. 新闻链接:⚠️ 重要提示
- "查看原文"链接应指向具体的新闻文章页面,而不是网站首页
- 如果使用WebSearch工具,确保提取的URL是完整的文章链接
- 手动创建数据时,需要提供真实的新闻文章URL
- 如果无法获取具体文章URL,页面会显示相应提示信息
常见问题
Q: 浏览器没有自动打开HTML页面怎么办?
A: 如果自动打开失败,可以使用以下方法手动打开:
方法1:文件资源管理器 1. 按 Win + E 打开文件资源管理器 2. 导航到:C:\Users\你的用户名\.claude\skills\news-summary\output 3. 双击HTML文件即可在默认浏览器中打开
方法2:PowerShell命令(推荐)
# 方式1:使用Start-Process
powershell -Command "Start-Process 'C:\Users\你的用户名\.claude\skills\news-summary\output\2025-12-08_14-30-00_主题\summary.html'"
# 方式2:直接在PowerShell中
Start-Process "C:\Users\你的用户名\.claude\skills\news-summary\output\2025-12-08_14-30-00_主题\summary.html"方法3:CMD命令
start "" "C:\Users\你的用户名\.claude\skills\news-summary\output\2025-12-08_14-30-00_主题\summary.html"方法4:指定浏览器打开
# Chrome
"C:\Program Files\Google\Chrome\Application\chrome.exe" "C:\Users\你的用户名\.claude\skills\news-summary\output\2025-12-08_14-30-00_主题\summary.html"
# Firefox
"C:\Program Files\Mozilla Firefox\firefox.exe" "C:\Users\你的用户名\.claude\skills\news-summary\output\2025-12-08_14-30-00_主题\summary.html"
# Edge
start msedge "C:\Users\你的用户名\.claude\skills\news-summary\output\2025-12-08_14-30-00_主题\summary.html"方法5:复制文件路径 1. 在文件资源管理器中右键点击HTML文件 2. 选择"复制为路径" 3. 打开任意浏览器,粘贴路径到地址栏 4. 按回车键打开
Q: AI解读按钮点击后没有反应?
A: 检查以下几点: 1. 确认server.js正在运行 2. 检查浏览器控制台是否有错误 3. 确认端口3456没有被其他程序占用
Q: 如何停止服务器?
A: 在运行server.js的终端中按 Ctrl+C
Q: 生成的HTML文件存放在哪里?
A: 在 ~/.claude/skills/news-summary/output/ 目录下
Q: 如何搜索特定时间范围的新闻?
A: 在搜索关键词中加入时间限制,例如:"人工智能 过去3天"
自定义扩展
添加更多导出格式
可以在 template.js 中添加新的HTML模板样式或在 server.js 中添加PDF、JSON等导出格式。
自定义权威性评估
修改 index.md 中的权威性评估逻辑,添加更多评估维度。
集成更多AI功能
可以扩展AI解读功能,添加:
- 情感分析
- 趋势预测
- 相关新闻推荐
- 多语言翻译
版本历史
- v1.0.0 (2025-12-08)
- 初始版本发布
- 支持新闻搜索和摘要
- 支持AI解读功能
- 自动生成HTML页面
许可证
MIT License
反馈与支持
如有问题或建议,请通过以下方式联系:
- 在Claude Code中使用
/help命令 - 访问 https://github.com/anthropics/claude-code/issues
Related skills
How it compares
Use news-summary for HTML digest generation; use a research skill when deep source analysis matters more than formatted news roundups.
FAQ
Where does news-summary save generated reports?
news-summary saves each run to a timestamped directory under output/, named with an ISO timestamp and topic slug. Optional analysis files land in an analysis subdirectory inside that folder.
Can news-summary add category navigation to digests?
news-summary supports a withCategories flag that adds category navigation to the generated HTML summary. Developers pass topic name and news data arrays to control layout and grouping.
Is News Summary safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.