diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 4f4530a..b56884a 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -1,11 +1,22 @@ # 追光AI 项目开发规则 ## 端口规范 -- 开发端口: 8301-8310 -- 默认端口: 8301 +- 应用端口范围: **8301-8310**(预留扩展) +- 默认端口: **8301** - 重启脚本仅扫描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 - Tailwind CSS + shadcn/ui @@ -114,7 +125,22 @@ ## 环境变量 - 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 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..33dae25 --- /dev/null +++ b/nginx.conf @@ -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'; + } +} \ No newline at end of file diff --git a/scripts/cron-daily-discover.sh b/scripts/cron-daily-discover.sh new file mode 100644 index 0000000..edab17d --- /dev/null +++ b/scripts/cron-daily-discover.sh @@ -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 "" diff --git a/scripts/cron-daily-news.sh b/scripts/cron-daily-news.sh new file mode 100644 index 0000000..77361f6 --- /dev/null +++ b/scripts/cron-daily-news.sh @@ -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 "" diff --git a/scripts/daily-news.mjs b/scripts/daily-news.mjs new file mode 100644 index 0000000..edcaad0 --- /dev/null +++ b/scripts/daily-news.mjs @@ -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 = /([\s\S]*?)<\/item>/g; + let match; + while ((match = itemRegex.exec(xml)) !== null) { + const block = match[1]; + const titleMatch = block.match(/<!\[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(); diff --git a/src/app/tools/page.tsx b/src/app/tools/page.tsx index 34d7be0..1719489 100644 --- a/src/app/tools/page.tsx +++ b/src/app/tools/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, Suspense } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -69,6 +69,14 @@ const categoryIcons: Record<string, string> = { }; 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 searchParams = useSearchParams();