diff --git a/README.md b/README.md index 0dc5976..f4dc5c5 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,11 @@ **内容成果展示:** -* 🎙️ **小宇宙**:[来生小酒馆](https://www.xiaoyuzhoufm.com/podcast/683c62b7c1ca9cf575a5030e) -* 📹 **抖音**:[来生情报站](https://www.douyin.com/user/MS4wLjABAAAAwpwqPQlu38sO38VyWgw9ZjDEnN4bMR5j8x111UxpseHR9DpB6-CveI5KRXOWuFwG) +| 🎙️ **小宇宙** | 📹 **抖音** | +| --- | --- | +| [来生小酒馆](https://www.xiaoyuzhoufm.com/podcast/683c62b7c1ca9cf575a5030e) | [来生情报站](https://www.douyin.com/user/MS4wLjABAAAAwpwqPQlu38sO38VyWgw9ZjDEnN4bMR5j8x111UxpseHR9DpB6-CveI5KRXOWuFwG)| +| ![小酒馆](docs/images/sm2.png "img") | ![情报站](docs/images/sm1.png "img") | + **项目截图:** diff --git a/src/foot.js b/src/foot.js new file mode 100644 index 0000000..9426e3c --- /dev/null +++ b/src/foot.js @@ -0,0 +1,15 @@ +export function insertFoot() { + return ` + + --- + + **收听语音版** + + | 🎙️ **小宇宙** | 📹 **抖音** | + | --- | --- | + | [来生小酒馆](https://www.xiaoyuzhoufm.com/podcast/683c62b7c1ca9cf575a5030e) | [来生情报站](https://www.douyin.com/user/MS4wLjABAAAAwpwqPQlu38sO38VyWgw9ZjDEnN4bMR5j8x111UxpseHR9DpB6-CveI5KRXOWuFwG)| + | ![小酒馆](https://raw.githubusercontent.com/justlovemaki/CloudFlare-AI-Insight-Daily/refs/heads/main/docs/images/sm2.png "img") | ![情报站](https://raw.githubusercontent.com/justlovemaki/CloudFlare-AI-Insight-Daily/refs/heads/main/docs/images/sm1.png "img") | + + + `; +} diff --git a/src/github.js b/src/github.js index e181cbb..c0ad1b5 100644 --- a/src/github.js +++ b/src/github.js @@ -87,4 +87,35 @@ export async function createOrUpdateGitHubFile(env, filePath, content, commitMes payload.sha = existingSha; } return callGitHubApi(env, `/contents/${filePath}`, 'PUT', payload); -} \ No newline at end of file +} + +/** + * Gets the content of a file from GitHub. + */ +export async function getDailyReportContent(env, filePath) { + const GITHUB_BRANCH = env.GITHUB_BRANCH || 'main'; + const GITHUB_REPO_OWNER = env.GITHUB_REPO_OWNER; + const GITHUB_REPO_NAME = env.GITHUB_REPO_NAME; + + if (!GITHUB_REPO_OWNER || !GITHUB_REPO_NAME) { + console.error("GitHub environment variables (GITHUB_REPO_OWNER, GITHUB_REPO_NAME) are not configured."); + throw new Error("GitHub API configuration is missing in environment variables."); + } + + const rawUrl = `https://raw.githubusercontent.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/${GITHUB_BRANCH}/${filePath}`; + console.log(rawUrl) + try { + const response = await fetch(rawUrl); + if (!response.ok) { + if (response.status === 404) { + console.log(`File not found: ${filePath} on branch ${GITHUB_BRANCH}`); + return null; + } + throw new Error(`Failed to fetch file from GitHub: ${response.status} ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error(`Error fetching daily report content from ${rawUrl}:`, error); + throw error; + } +} diff --git a/src/handlers/commitToGitHub.js b/src/handlers/commitToGitHub.js index 0109948..e10297b 100644 --- a/src/handlers/commitToGitHub.js +++ b/src/handlers/commitToGitHub.js @@ -1,6 +1,9 @@ // src/handlers/commitToGitHub.js -import { getISODate, formatMarkdownText } from '../helpers.js'; +import { getISODate, formatMarkdownText, replaceImageProxy,formatDateToChineseWithTime } from '../helpers.js'; import { getGitHubFileSha, createOrUpdateGitHubFile } from '../github.js'; +import { storeInKV } from '../kv.js'; +import { marked } from '../marked.esm.js'; + export async function handleCommitToGitHub(request, env) { if (request.method !== 'POST') { return new Response(JSON.stringify({ status: 'error', message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); @@ -10,11 +13,21 @@ export async function handleCommitToGitHub(request, env) { const dateStr = formData.get('date') || getISODate(); const dailyMd = formData.get('daily_summary_markdown'); const podcastMd = formData.get('podcast_script_markdown'); + const report = { + report_date: dateStr, + title: dateStr+'日刊', + link: '/daily/'+dateStr+'.html', + content_html: null, + // 可以添加其他相關欄位,例如作者、來源等 + published_date: formatDateToChineseWithTime(new Date()) // 記錄保存時間 + } const filesToCommit = []; if (dailyMd) { filesToCommit.push({ path: `daily/${dateStr}.md`, content: formatMarkdownText(dailyMd), description: "Daily Summary File" }); + report.content_html = marked.parse(formatMarkdownText(replaceImageProxy(env.IMG_PROXY, dailyMd))); + storeInKV(env.DATA_KV, `${dateStr}-report`, report); } if (podcastMd) { filesToCommit.push({ path: `podcast/${dateStr}.md`, content: podcastMd, description: "Podcast Script File" }); diff --git a/src/handlers/genAIContent.js b/src/handlers/genAIContent.js index a6a7e5a..d408550 100644 --- a/src/handlers/genAIContent.js +++ b/src/handlers/genAIContent.js @@ -8,6 +8,7 @@ import { getSystemPromptSummarizationStepOne } from '../prompt/summarizationProm import { getSystemPromptSummarizationStepTwo } from '../prompt/summarizationPromptStepTwo.js'; import { getSystemPromptPodcastFormatting } from '../prompt/podcastFormattingPrompt.js'; import { getSystemPromptDailyAnalysis } from '../prompt/dailyAnalysisPrompt.js'; // Import new prompt +import { insertFoot } from '../foot.js'; export async function handleGenAIPodcastScript(request, env) { let dateStr; @@ -228,6 +229,7 @@ export async function handleGenAIContent(request, env) { if (fullPromptForCall2_User) promptsMarkdownContent += `### User Input (Output of Call 1)\n\`\`\`\n${fullPromptForCall2_User}\n\`\`\`\n\n`; let dailySummaryMarkdownContent = `# ${env.DAILY_TITLE} ${formatDateToChinese(dateStr)}\n\n${removeMarkdownCodeBlock(outputOfCall2)}`; + if (env.INSERT_FOOT) dailySummaryMarkdownContent += insertFoot() +`\n\n`; const successHtml = generateGenAiPageHtml( env, diff --git a/src/handlers/getRss.js b/src/handlers/getRss.js new file mode 100644 index 0000000..6e4c94d --- /dev/null +++ b/src/handlers/getRss.js @@ -0,0 +1,99 @@ +import { stripHtml, getShanghaiTime, formatRssDate } from '../helpers.js'; +import { getFromKV } from '../kv.js'; + +function minifyHTML(htmlString) { + if (typeof htmlString !== 'string') { + return ''; + } + + return htmlString + .replace(/>\s+<') // 移除标签之间的空白 + .trim(); // 移除字符串两端的空白 +} + +/** + * 處理 Supabase RSS 請求 + * @param {Request} request - 傳入的請求物件 + * @param {object} env - Cloudflare Workers 環境變數 + * @returns {Response} RSS Feed 的回應 + */ +export async function handleRss(request, env) { + const url = new URL(request.url); + const days = parseInt(url.searchParams.get('days')) || 7; // 預設查詢 7 天內的資料 + + const allData = []; + const today = getShanghaiTime(); // 加上東八時區的偏移量 + console.log(today); + + for (let i = 0; i < days; i++) { + const date = new Date(today); + date.setDate(today.getDate() - i); + const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD + const key = `${dateStr}-report`; + const data = await getFromKV(env.DATA_KV, key); + if (data) { + allData.push(data); + } + } + + // 扁平化數據,因為每個 report 可能包含多個項目 + const data = allData.flat(); + + if (!data || data.length === 0) { + return new Response('沒有找到相關資料', { status: 200 }); + } + + // 建立 RSS Feed + let rssItems = ''; + if (data && data.length > 0) { + const filteredData = {}; + data.forEach(item => { + const reportDate = item.report_date; + const publishedDate = new Date(item.published_date); + + if (!filteredData[reportDate] || publishedDate > new Date(filteredData[reportDate].published_date)) { + filteredData[reportDate] = item; + } + }); + const finalData = Object.values(filteredData); + + finalData.forEach(item => { + const pubDate = item.published_date ? formatRssDate(new Date(item.published_date)) : formatRssDate(new Date()); + const content = minifyHTML(item.content_html); + const title = item.title || '无标题'; + const link = env.BOOK_LINK+item.link || '#'; + const description = stripHtml(item.content_html).substring(0, 200); // 移除 HTML 標籤並截取 200 字元 + + rssItems += ` + + <![CDATA[${title}]]> + ${link} + ${item.id || link} + ${pubDate} + + + + `; + }); + } + + const rssFeed = ` + + + AI洞察日报 RSS Feed + ${env.BOOK_LINK} + 近 ${days} 天的AI日报 + zh-cn + ${formatRssDate(new Date())} + + ${rssItems} + +`; + + return new Response(rssFeed, { + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + 'Cache-Control': 'public, max-age=3600' // 快取一小時 + } + }); +} diff --git a/src/handlers/writeRssData.js b/src/handlers/writeRssData.js new file mode 100644 index 0000000..311e4d0 --- /dev/null +++ b/src/handlers/writeRssData.js @@ -0,0 +1,39 @@ +import { replaceImageProxy, formatDateToChineseWithTime } from '../helpers.js'; +import { getDailyReportContent } from '../github.js'; +import { storeInKV } from '../kv.js'; +import { marked } from '../marked.esm.js'; + +export async function handleWriteRssData(request, env) { + const url = new URL(request.url); + const dateStr = url.searchParams.get('date'); + + if (!dateStr) { + return new Response('Missing date parameter', { status: 400 }); + } + + try { + const path = `daily/${dateStr}.md`; + const content = await getDailyReportContent(env, path); + if (!content) { + return new Response(`No content found for ${path}`, { status: 404 }); + } + + const report = { + report_date: dateStr, + title: dateStr+'日刊', + link: '/daily/'+dateStr+'.html', + content_html: null, + // 可以添加其他相關欄位,例如作者、來源等 + published_date: formatDateToChineseWithTime(new Date()) // 記錄保存時間 + } + report.content_html = marked.parse(replaceImageProxy(env.IMG_PROXY, content)); + storeInKV(env.DATA_KV, `${dateStr}-report`, report); + + return new Response(JSON.stringify({ message: `Successfully fetched and stored daily report for ${dateStr}`}), { + headers: { 'Content-Type': 'application/json' }, + status: 200 + }); + } catch (error) { + console.error('Error handling daily report:', error); + } +} \ No newline at end of file diff --git a/src/helpers.js b/src/helpers.js index bfd431e..5829540 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -121,7 +121,7 @@ export function stripHtml(html) { * @param {string} dateString - The date string to convert. * @returns {Date} A Date object set to the specified date in Asia/Shanghai timezone. */ -function convertToShanghaiTime(dateString) { +export function convertToShanghaiTime(dateString) { // Create a Date object from the ISO string. const date = new Date(dateString); @@ -143,6 +143,28 @@ function convertToShanghaiTime(dateString) { return new Date(shanghaiDateString); } +export function getShanghaiTime() { + // Create a Date object from the ISO string. + const date = new Date(); + + // Get the date components in Asia/Shanghai timezone + const options = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + timeZone: 'Asia/Shanghai' + }; + + // Format the date to a string in Shanghai timezone, then parse it back to a Date object. + // This is a common workaround to get a Date object representing a specific timezone. + const shanghaiDateString = new Intl.DateTimeFormat('en-US', options).format(date); + return new Date(shanghaiDateString); +} + /** * Checks if a given date string is within the last specified number of days (inclusive of today). * @param {string} dateString - The date string to check (YYYY-MM-DD or ISO format). @@ -202,6 +224,27 @@ export function formatDateToChineseWithTime(isoDateString) { return new Intl.DateTimeFormat('zh-CN', options).format(date); } +/** + * 將日期物件格式化為 RSS 2.0 規範的日期字串 (RFC 822) + * 例如: "Thu, 01 Jan 1970 00:00:00 GMT" + * @param {Date} date - 日期物件 + * @returns {string} 格式化後的日期字串 + */ +export function formatRssDate(date) { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + const dayOfWeek = days[date.getUTCDay()]; + const dayOfMonth = date.getUTCDate(); + const month = months[date.getUTCMonth()]; + const year = date.getUTCFullYear(); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + const seconds = String(date.getUTCSeconds()).padStart(2, '0'); + + return `${dayOfWeek}, ${dayOfMonth} ${month} ${year} ${hours}:${minutes}:${seconds} GMT`; + } + /** * Converts English double quotes (") to Chinese double quotes (“”). * @param {string} text - The input string. diff --git a/src/htmlGenerators.js b/src/htmlGenerators.js index f9ee440..7dc68f7 100644 --- a/src/htmlGenerators.js +++ b/src/htmlGenerators.js @@ -1,6 +1,7 @@ // src/htmlGenerators.js import { escapeHtml, formatDateToChinese, convertEnglishQuotesToChinese, replaceImageProxy} from './helpers.js'; import { dataSources } from './dataFetchers.js'; // Import dataSources +import { marked } from './marked.esm.js'; function generateHtmlListForContentPage(items, dateStr) { let listHtml = ''; @@ -395,7 +396,7 @@ export function generateGenAiPageHtml(env, title, bodyContent, pageDate, isError

所选内容日期: ${formatDateToChinese(escapeHtml(pageDate))}

${bodyContent}
-
${markdownToHtml(replaceImageProxy(env.IMG_PROXY, bodyContent))}
+
${marked.parse(replaceImageProxy(env.IMG_PROXY, bodyContent))}
${promptDisplayHtml}