feat: 部署配置+真实新闻源+定时任务+域名SSL
This commit is contained in:
@@ -1,11 +1,22 @@
|
|||||||
# 追光AI 项目开发规则
|
# 追光AI 项目开发规则
|
||||||
|
|
||||||
## 端口规范
|
## 端口规范
|
||||||
- 开发端口: 8301-8310
|
- 应用端口范围: **8301-8310**(预留扩展)
|
||||||
- 默认端口: 8301
|
- 默认端口: **8301**
|
||||||
- 重启脚本仅扫描8301-8310范围
|
- 重启脚本仅扫描8301-8310范围
|
||||||
- **强制要求:本项目端口必须锁定在8301-8310区间内,禁止使用其他端口,因为本服务器同时运行其他产品**
|
- **强制要求:本项目端口必须锁定在8301-8310区间内,禁止使用其他端口,因为本服务器同时运行其他产品**
|
||||||
|
|
||||||
|
## 服务器与基础设施
|
||||||
|
- **域名**: www.zhuig.com (HTTPS,SSL证书自动续期)
|
||||||
|
- **云服务器**: 119.45.242.239 (登录名: ubuntu, 密钥: E:\ZG_Dev\pem\zg.pem)
|
||||||
|
- **数据库**: 阿里云RDS MySQL — rm-0jlbgr2rv6dj3t6jngo.mysql.rds.aliyuncs.com:3306
|
||||||
|
- **Redis**: 云服务器上已有(Docker容器redis-cache:6379),与其他项目共用,有密码认证
|
||||||
|
- **本地Git同步目录**: K:\git-repos
|
||||||
|
- **开发机**: E:\ZG_Dev\ZhuiGuangAI\zhuiguang-ai
|
||||||
|
- **部署目录**: /home/ubuntu/zhuiguang-ai
|
||||||
|
- **访问地址**: https://www.zhuig.com (HTTP自动跳转HTTPS)
|
||||||
|
- **注意事项**: 服务器有其他应用运行,部署时不可占用非8301-8310端口,不可影响其他服务
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
- Next.js 14 (App Router) + TypeScript
|
- Next.js 14 (App Router) + TypeScript
|
||||||
- Tailwind CSS + shadcn/ui
|
- Tailwind CSS + shadcn/ui
|
||||||
@@ -114,7 +125,22 @@
|
|||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
- GITHUB_TOKEN: GitHub Personal Access Token(可选,提升API限流从60→5000次/小时)
|
- GITHUB_TOKEN: GitHub Personal Access Token(可选,提升API限流从60→5000次/小时)
|
||||||
- DEEPSEEK_API_KEY: DeepSeek API Key(用于技能评测生成脚本)
|
- DEEPSEEK_API_KEY: DeepSeek API Key(用于技能评测生成脚本、AI日报整理)
|
||||||
|
|
||||||
|
## 定时任务(Crontab)
|
||||||
|
- **AI日报生成**: 每天 7:30 — `scripts/daily-news.mjs`(搜索真实新闻 + DeepSeek整理格式)
|
||||||
|
- **技能自动发现**: 每天 2:00 — `scripts/daily-discover.mjs`(GitHub搜索 + DeepSeek评测 + 入库)
|
||||||
|
- 运行脚本: `/home/ubuntu/zhuiguang-ai/scripts/cron-daily-news.sh` / `cron-daily-discover.sh`
|
||||||
|
- 日志目录: `/home/ubuntu/zhuiguang-ai/logs/`
|
||||||
|
|
||||||
|
## AI日报新闻源
|
||||||
|
- **36kr** RSS (https://36kr.com/feed) — 国内科技资讯
|
||||||
|
- **IT之家** RSS (https://www.ithome.com/rss/) — 国内科技资讯
|
||||||
|
- **开源中国** RSS (https://www.oschina.net/news/rss) — 开源动态
|
||||||
|
- **HackerNews** API — 海外AI/技术热点
|
||||||
|
- **Google News** RSS — 海外新闻(国内服务器可能无法访问)
|
||||||
|
- 流程: 搜索真实新闻 → AI关键词过滤 → 近2天筛选 → DeepSeek整理格式 → 入库发布
|
||||||
|
- **严禁让DeepSeek直接生成新闻内容**(会编造假新闻),必须基于真实新闻素材整理
|
||||||
|
|
||||||
## 踩坑记录
|
## 踩坑记录
|
||||||
- Next.js 14不支持useActionState,登录页用原生表单POST
|
- Next.js 14不支持useActionState,登录页用原生表单POST
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name 119.45.242.239;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8301;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/auth {
|
||||||
|
proxy_pass http://127.0.0.1:8301;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /_next/static {
|
||||||
|
proxy_pass http://127.0.0.1:8301;
|
||||||
|
expires 365d;
|
||||||
|
add_header Cache-Control 'public, immutable';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/ubuntu/zhuiguang-ai
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
|
||||||
|
echo "========== $(date '+%Y-%m-%d %H:%M:%S') 每日技能发现 =========="
|
||||||
|
node scripts/daily-discover.mjs >> /home/ubuntu/zhuiguang-ai/logs/daily-discover.log 2>&1
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/ubuntu/zhuiguang-ai
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
|
||||||
|
echo "========== $(date '+%Y-%m-%d %H:%M:%S') 每日新闻生成 =========="
|
||||||
|
node scripts/daily-news.mjs >> /home/ubuntu/zhuiguang-ai/logs/daily-news.log 2>&1
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
|
||||||
|
import OpenAI from "openai";
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
|
const base = process.env.DATABASE_URL || "mysql://localhost:3306/zhuiguang_ai?charset=utf8mb4";
|
||||||
|
const sep = base.includes("?") ? "&" : "?";
|
||||||
|
const connectionString = `${base}${sep}connection_limit=20&pool_timeout=30`;
|
||||||
|
const adapter = new PrismaMariaDb(connectionString);
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
|
baseURL: "https://api.deepseek.com/v1",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchRSS(rssUrl, source) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(rssUrl, {
|
||||||
|
headers: { "User-Agent": "Mozilla/5.0 (compatible; NewsBot/1.0)" },
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const xml = await response.text();
|
||||||
|
const items = [];
|
||||||
|
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
||||||
|
let match;
|
||||||
|
while ((match = itemRegex.exec(xml)) !== null) {
|
||||||
|
const block = match[1];
|
||||||
|
const titleMatch = block.match(/<title><!\[CDATA\[([\s\S]*?)\]\]><\/title>/) || block.match(/<title>([\s\S]*?)<\/title>/);
|
||||||
|
const linkMatch = block.match(/<link>([\s\S]*?)<\/link>/);
|
||||||
|
const descMatch = block.match(/<description><!\[CDATA\[([\s\S]*?)\]\]><\/description>/) || block.match(/<description>([\s\S]*?)<\/description>/);
|
||||||
|
const pubDateMatch = block.match(/<pubDate>([\s\S]*?)<\/pubDate>/) || block.match(/<published>([\s\S]*?)<\/published>/);
|
||||||
|
if (titleMatch) {
|
||||||
|
items.push({
|
||||||
|
title: titleMatch[1].trim(),
|
||||||
|
url: linkMatch ? linkMatch[1].trim() : "",
|
||||||
|
content: descMatch ? descMatch[1].replace(/<[^>]+>/g, "").trim() : "",
|
||||||
|
pubDate: pubDateMatch ? pubDateMatch[1].trim() : "",
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ⚠️ ${source} RSS获取失败: ${e.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch36krAI() {
|
||||||
|
return fetchRSS("https://36kr.com/feed", "36kr");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchITHomeAI() {
|
||||||
|
return fetchRSS("https://www.ithome.com/rss/", "IT之家");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOSChinaAI() {
|
||||||
|
return fetchRSS("https://www.oschina.net/news/rss", "开源中国");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGoogleNewsRSS(query, lang = "zh-CN", country = "CN") {
|
||||||
|
const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=${lang}&gl=${country}&ceid=${country}:${lang === "zh-CN" ? "zh-Hans" : "en"}`;
|
||||||
|
const results = await fetchRSS(url, `GoogleNews-${lang}`);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchHackerNewsTop() {
|
||||||
|
try {
|
||||||
|
const topRes = await fetch("https://hacker-news.firebaseio.com/v0/topstories.json");
|
||||||
|
const topIds = await topRes.json();
|
||||||
|
const aiIds = topIds.slice(0, 60);
|
||||||
|
const items = [];
|
||||||
|
for (const id of aiIds) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);
|
||||||
|
const story = await res.json();
|
||||||
|
if (!story || !story.title) continue;
|
||||||
|
const title = story.title.toLowerCase();
|
||||||
|
if (
|
||||||
|
title.includes("ai") ||
|
||||||
|
title.includes("llm") ||
|
||||||
|
title.includes("gpt") ||
|
||||||
|
title.includes("openai") ||
|
||||||
|
title.includes("deepseek") ||
|
||||||
|
title.includes("machine learning") ||
|
||||||
|
title.includes("neural") ||
|
||||||
|
title.includes("model") ||
|
||||||
|
title.includes("claude") ||
|
||||||
|
title.includes("gemini") ||
|
||||||
|
title.includes("agent") ||
|
||||||
|
title.includes("rag") ||
|
||||||
|
title.includes("transformer")
|
||||||
|
) {
|
||||||
|
items.push({
|
||||||
|
title: story.title,
|
||||||
|
url: story.url || `https://news.ycombinator.com/item?id=${story.id}`,
|
||||||
|
content: "",
|
||||||
|
pubDate: new Date(story.time * 1000).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (items.length >= 10) break;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ⚠️ HackerNews获取失败: ${e.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchAllNews() {
|
||||||
|
console.log("🔍 第一步:搜索真实AI新闻...");
|
||||||
|
|
||||||
|
const allResults = [];
|
||||||
|
const seenTitles = new Set();
|
||||||
|
|
||||||
|
function addResults(results, source) {
|
||||||
|
for (const r of results) {
|
||||||
|
const key = r.title.substring(0, 30).toLowerCase();
|
||||||
|
if (!seenTitles.has(key) && r.title) {
|
||||||
|
seenTitles.add(key);
|
||||||
|
allResults.push({ ...r, source });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(" 📡 36kr...");
|
||||||
|
const kr36 = await fetch36krAI();
|
||||||
|
addResults(kr36, "36kr");
|
||||||
|
console.log(` 获取 ${kr36.length} 条`);
|
||||||
|
|
||||||
|
console.log(" 📡 IT之家...");
|
||||||
|
const ithome = await fetchITHomeAI();
|
||||||
|
addResults(ithome, "IT之家");
|
||||||
|
console.log(` 获取 ${ithome.length} 条`);
|
||||||
|
|
||||||
|
console.log(" 📡 开源中国...");
|
||||||
|
const oschina = await fetchOSChinaAI();
|
||||||
|
addResults(oschina, "开源中国");
|
||||||
|
console.log(` 获取 ${oschina.length} 条`);
|
||||||
|
|
||||||
|
console.log(" 📡 Google News (中文)...");
|
||||||
|
const zhNews = await fetchGoogleNewsRSS("人工智能 AI 最新", "zh-CN", "CN");
|
||||||
|
addResults(zhNews, "GoogleNews-CN");
|
||||||
|
console.log(` 获取 ${zhNews.length} 条`);
|
||||||
|
|
||||||
|
console.log(" 📡 Google News (English)...");
|
||||||
|
const enNews = await fetchGoogleNewsRSS("artificial intelligence AI latest news", "en", "US");
|
||||||
|
addResults(enNews, "GoogleNews-EN");
|
||||||
|
console.log(` 获取 ${enNews.length} 条`);
|
||||||
|
|
||||||
|
console.log(" 📡 HackerNews Top AI...");
|
||||||
|
const hnNews = await fetchHackerNewsTop();
|
||||||
|
addResults(hnNews, "HackerNews");
|
||||||
|
console.log(` 获取 ${hnNews.length} 条`);
|
||||||
|
|
||||||
|
const AI_KEYWORDS = [
|
||||||
|
"ai", "人工智能", "大模型", "llm", "gpt", "chatgpt", "openai", "deepseek",
|
||||||
|
"claude", "gemini", "通义", "文心", "智谱", "kimi", "豆包", "百川",
|
||||||
|
"diffusion", "stable diffusion", "midjourney", "sora",
|
||||||
|
"机器学习", "深度学习", "神经网络", "transformer",
|
||||||
|
"copilot", "agent", "rag", "aigc", "生成式",
|
||||||
|
"自动驾驶", "智能体", "多模态", "语音识别",
|
||||||
|
];
|
||||||
|
|
||||||
|
function isAIRelated(title) {
|
||||||
|
const lower = title.toLowerCase();
|
||||||
|
return AI_KEYWORDS.some((kw) => lower.includes(kw));
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentResults = allResults
|
||||||
|
.filter((r) => {
|
||||||
|
if (!isAIRelated(r.title)) return false;
|
||||||
|
if (!r.pubDate) return true;
|
||||||
|
try {
|
||||||
|
const pubTime = new Date(r.pubDate).getTime();
|
||||||
|
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000;
|
||||||
|
return pubTime > twoDaysAgo;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalResults = recentResults.length > 0 ? recentResults : allResults.slice(0, 30);
|
||||||
|
console.log(` ✅ 共获取 ${allResults.length} 条,近2天 ${recentResults.length} 条,使用 ${finalResults.length} 条`);
|
||||||
|
return finalResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMAT_PROMPT = `你是AI行业资深分析师。以下是从互联网搜索到的今日真实AI新闻素材,请基于这些真实素材整理一份AI日报。
|
||||||
|
|
||||||
|
严格要求:
|
||||||
|
1. 只能基于提供的素材内容,绝对不能编造任何新闻!如果素材不够6条,就只输出有素材支持的条目
|
||||||
|
2. 每条新闻必须保留原始来源URL
|
||||||
|
3. 标题简洁有力(20字以内),摘要精炼(50字以内),详情深入(100-200字)
|
||||||
|
4. 优先关注中国AI行业动态,兼顾全球重要事件
|
||||||
|
5. 分类标注规则(必须使用以下key值):
|
||||||
|
- model:模型前沿(LLM/多模态新模型发布、架构创新、训练方法突破)
|
||||||
|
- product:产品落地(AI产品发布/更新、C端应用上线、B端解决方案、新功能)
|
||||||
|
- opensource:开源生态(开源项目动态、框架更新、开源社区大事件)
|
||||||
|
- business:商业产业(投融资、收购并购、财报营收、公司战略、行业趋势)
|
||||||
|
- policy:政策治理(AI法规政策、监管动态、伦理治理、国际AI治理)
|
||||||
|
- insight:深度洞察(行业领袖观点、深度分析报告、学术研究前沿)
|
||||||
|
|
||||||
|
新闻素材:
|
||||||
|
{newsData}
|
||||||
|
|
||||||
|
返回纯JSON格式:
|
||||||
|
{
|
||||||
|
"title": "AI日报:{date} 核心要点概览",
|
||||||
|
"summary": "今日AI行业整体趋势概述(100字以内)",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"category": "model",
|
||||||
|
"title": "新闻标题",
|
||||||
|
"summary": "一句话摘要",
|
||||||
|
"detail": "详细内容描述",
|
||||||
|
"sourceUrl": "原始来源URL",
|
||||||
|
"hotScore": 85
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
hotScore热度评分规则(0-100):
|
||||||
|
- 90-100:全球头条级事件
|
||||||
|
- 70-89:行业重要事件
|
||||||
|
- 50-69:值得关注的动态
|
||||||
|
- 30-49:一般性新闻`;
|
||||||
|
|
||||||
|
function extractJSON(str) {
|
||||||
|
const trimmed = str.trim();
|
||||||
|
const codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
|
let jsonStr = codeBlockMatch ? codeBlockMatch[1].trim() : trimmed;
|
||||||
|
const braceMatch = jsonStr.match(/\{[\s\S]*\}/);
|
||||||
|
if (braceMatch) {
|
||||||
|
try { return JSON.parse(braceMatch[0]); } catch {}
|
||||||
|
}
|
||||||
|
const arrayMatch = jsonStr.match(/\[[\s\S]*\]/);
|
||||||
|
if (arrayMatch) {
|
||||||
|
try { return JSON.parse(arrayMatch[0]); } catch {}
|
||||||
|
}
|
||||||
|
return JSON.parse(jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateDailyNews() {
|
||||||
|
const today = new Date();
|
||||||
|
const dateStr = today.toLocaleDateString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
const dateISO = today.toISOString().split("T")[0];
|
||||||
|
|
||||||
|
console.log(`📰 开始生成 ${dateStr} 的AI日报...`);
|
||||||
|
|
||||||
|
const existing = await prisma.dailyReport.findFirst({
|
||||||
|
where: { date: new Date(dateISO) },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
console.log(`⚠️ ${dateISO} 已有日报 (ID:${existing.id}),先删除旧版`);
|
||||||
|
await prisma.dailyReport.delete({ where: { id: existing.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newsResults = await searchAllNews();
|
||||||
|
if (newsResults.length === 0) {
|
||||||
|
throw new Error("未搜索到任何新闻素材,无法生成日报");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newsDataText = newsResults
|
||||||
|
.slice(0, 30)
|
||||||
|
.map((r, i) => `[${i + 1}] 标题: ${r.title}\n 内容: ${r.content || "(无摘要)"}\n 来源: ${r.url}\n 时间: ${r.pubDate || "未知"}`)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
console.log("🤖 第二步:调用 DeepSeek 整理新闻格式...");
|
||||||
|
const prompt = FORMAT_PROMPT
|
||||||
|
.replace(/\{newsData\}/g, newsDataText)
|
||||||
|
.replace(/\{date\}/g, dateStr);
|
||||||
|
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: "deepseek-v4-pro",
|
||||||
|
messages: [{ role: "user", content: prompt }],
|
||||||
|
temperature: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = completion.choices[0]?.message?.content;
|
||||||
|
if (!content) {
|
||||||
|
throw new Error("DeepSeek 返回内容为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = extractJSON(content);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ JSON解析失败,原始内容前500字:");
|
||||||
|
console.error(content.substring(0, 500));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.items || !Array.isArray(parsed.items) || parsed.items.length === 0) {
|
||||||
|
throw new Error("整理后的日报没有有效的新闻条目");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 整理成功,共 ${parsed.items.length} 条新闻`);
|
||||||
|
|
||||||
|
const report = await prisma.dailyReport.create({
|
||||||
|
data: {
|
||||||
|
date: new Date(dateISO),
|
||||||
|
title: parsed.title || `AI日报:${dateStr} 核心要点概览`,
|
||||||
|
summary: parsed.summary || "",
|
||||||
|
hotCount: parsed.items.length,
|
||||||
|
status: "published",
|
||||||
|
items: {
|
||||||
|
create: parsed.items.map((item, index) => ({
|
||||||
|
sortOrder: index,
|
||||||
|
category: item.category || "product",
|
||||||
|
title: item.title || "未命名新闻",
|
||||||
|
summary: item.summary || "",
|
||||||
|
detail: item.detail || "",
|
||||||
|
sourceUrl: item.sourceUrl || "",
|
||||||
|
imageUrl: item.imageUrl || "",
|
||||||
|
hotScore: item.hotScore || 50,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n🎉 日报生成完成!`);
|
||||||
|
console.log(` ID: ${report.id}`);
|
||||||
|
console.log(` 标题: ${report.title}`);
|
||||||
|
console.log(` 日期: ${dateISO}`);
|
||||||
|
console.log(` 条目数: ${report.items.length}`);
|
||||||
|
console.log(` 状态: ${report.status}`);
|
||||||
|
|
||||||
|
report.items.forEach((item, i) => {
|
||||||
|
console.log(` ${i + 1}. [${item.category}] ${item.title} (热度:${item.hotScore})`);
|
||||||
|
if (item.sourceUrl) {
|
||||||
|
console.log(` 来源: ${item.sourceUrl}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await generateDailyNews();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 日报生成失败:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, Suspense } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
@@ -69,6 +69,14 @@ const categoryIcons: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ToolsPage() {
|
export default function ToolsPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="flex min-h-screen items-center justify-center"><p className="text-muted-foreground">加载中...</p></div>}>
|
||||||
|
<ToolsContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolsContent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user