feat: 添加RSS订阅功能及页脚支持

- 新增RSS订阅功能,支持获取最近7天的日报内容
- 添加页脚插入功能,包含播客平台链接和图片
- 实现GitHub文件内容获取接口
- 优化日期处理工具函数,增加RSS日期格式支持
- 使用marked.js替换原有markdown解析器
- 在提交到GitHub时同时存储报告数据到KV
This commit is contained in:
justlovemaki
2025-06-14 22:18:26 +08:00
parent 101453894f
commit 1841248fec
12 changed files with 2472 additions and 303 deletions

View File

@@ -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") |
**项目截图:**

15
src/foot.js Normal file
View File

@@ -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") |
`;
}

View File

@@ -87,4 +87,35 @@ export async function createOrUpdateGitHubFile(env, filePath, content, commitMes
payload.sha = existingSha;
}
return callGitHubApi(env, `/contents/${filePath}`, 'PUT', payload);
}
}
/**
* 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;
}
}

View File

@@ -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" });

View File

@@ -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,

99
src/handlers/getRss.js Normal file
View File

@@ -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+</g, '><') // 移除标签之间的空白
.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 += `
<item>
<title><![CDATA[${title}]]></title>
<link>${link}</link>
<guid>${item.id || link}</guid>
<pubDate>${pubDate}</pubDate>
<content:encoded><![CDATA[${content}]]></content:encoded>
<description><![CDATA[${description}]]></description>
</item>
`;
});
}
const rssFeed = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>AI洞察日报 RSS Feed</title>
<link>${env.BOOK_LINK}</link>
<description> 近 ${days} 天的AI日报</description>
<language>zh-cn</language>
<lastBuildDate>${formatRssDate(new Date())}</lastBuildDate>
<atom:link href="${url.origin}/rss" rel="self" type="application/rss+xml" />
${rssItems}
</channel>
</rss>`;
return new Response(rssFeed, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=3600' // 快取一小時
}
});
}

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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
</div>
<p>所选内容日期: <strong>${formatDateToChinese(escapeHtml(pageDate))}</strong></p>
<div class="content-box" id="mainContentBox">${bodyContent}</div>
<div class="content-box" id="outContentBox">${markdownToHtml(replaceImageProxy(env.IMG_PROXY, bodyContent))}</div>
<div class="content-box" id="outContentBox">${marked.parse(replaceImageProxy(env.IMG_PROXY, bodyContent))}</div>
${promptDisplayHtml}
<div class="navigation-links">
<a href="/getContentHtml?date=${encodeURIComponent(pageDate)}" class="button-link">返回内容选择</a>
@@ -447,27 +448,36 @@ export function generateGenAiPageHtml(env, title, bodyContent, pageDate, isError
formData.append('podcast_script_markdown', document.getElementById('podcastMd').value);
}
let githubSuccess = false;
let supabaseSuccess = false;
try {
const response = await fetch('/commitToGitHub', {
// Commit to GitHub
const githubResponse = await fetch('/commitToGitHub', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
const githubResult = await githubResponse.json();
if (githubResponse.ok) {
alert('GitHub 提交成功!');
console.log('GitHub Commit Success:', result);
console.log('GitHub Commit Success:', githubResult);
githubSuccess = true;
} else {
alert('GitHub 提交失败: ' + result.message);
console.error('GitHub Commit Failed:', result);
alert('GitHub 提交失败: ' + githubResult.message);
console.error('GitHub Commit Failed:', githubResult);
}
} catch (error) {
console.error('Error committing to GitHub:', error);
alert('请求失败,请检查网络或服务器。');
} finally {
button.textContent = originalText;
button.disabled = false;
alert('GitHub 请求失败,请检查网络或服务器。');
}
if (githubSuccess || supabaseSuccess) {
// Optionally reload or update UI if both or one succeeded
}
button.textContent = originalText;
button.disabled = false;
}
async function generateAIDailyAnalysis(date) {
@@ -514,286 +524,3 @@ export function generateGenAiPageHtml(env, title, bodyContent, pageDate, isError
</script>
</body></html>`;
}
/**
* 一个功能完善的 Markdown 到 HTML 的转换器。
*
* 设计思路:
* 1. **tokenize(markdown)**: 词法分析器,将 Markdown 字符串转换为一个令牌数组。
* - 它按块block-level elements处理输入如段落、标题、列表等。
* - 每个令牌是一个对象,如 { type: 'heading', depth: 1, text: '标题文字' }。
* - 包含内联 Markdown 的文本(如段落内容)会先保持原样,等待下一步处理。
*
* 2. **render(tokens)**: 渲染器,接收令牌数组并输出 HTML。
* - 它遍历令牌,根据令牌的 `type` 生成相应的 HTML 标签。
* - 当遇到需要处理内联元素的令牌时(如 heading, paragraph它会调用 `parseInline`。
*
* 3. **parseInline(text)**: 内联解析器。
* - 它负责处理行内的 Markdown 语法,如加粗、斜体、链接、图片、行内代码等。
* - 它使用一系列的正则表达式按优先级顺序进行替换。
*
* 4. **辅助函数**: 如 `escapeHtml` 用于防止 XSS 攻击。
*/
export function markdownToHtml(markdown) {
if (typeof markdown !== 'string') {
console.error("Input must be a string.");
return '';
}
// 预处理:规范化换行符,确保尾部有换行符以便于正则匹配
const preprocessedMarkdown = markdown.replace(/\r\n?/g, '\n').replace(/^(#+\s*[^#\s].*)\s*#+\s*$/gm, '$1') + '\n\n';
// --- 1. 辅助函数 ---
const escapeHtml = (str) => {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '\''
};
return str.replace(/[&<>"']/g, m => map[m]);
};
// --- 2. 内联解析器 (Inline Parser) ---
// 按优先级顺序处理内联元素
const parseInline = (text) => {
let html = text;
// 图片: ![alt](src "title")
html = html.replace(/!\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_, alt, src, title) => {
let titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"${titleAttr}>`;
});
// 链接: [text](href "title")
html = html.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_, text, href, title) => {
let titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
return `<a href="${escapeHtml(href)}"${titleAttr}>${parseInline(text)}</a>`; // 递归处理链接文本中的内联格式
});
// 行内代码: `code`
html = html.replace(/`([^`]+)`/g, (_, code) => `<code>${escapeHtml(code)}</code>`);
// 加粗+斜体: ***text*** 或 ___text___
html = html.replace(/(\*\*\*|___)(.+?)\1/g, '<strong><em>$2</em></strong>');
// 加粗: **text** 或 __text__
html = html.replace(/(\*\*|__)(.+?)\1/g, '<strong>$2</strong>');
// 斜体: *text* 或 _text_
// html = html.replace(/(\*|_)(.+?)\1/g, '<em>$2</em>');
// 删除线: ~~text~~
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
// 换行: 行尾两个空格
html = html.replace(/ {2,}\n/g, '<br>\n');
return html;
};
// --- 3. 词法分析器 (Tokenizer) ---
const tokenize = (md) => {
const tokens = [];
let src = md;
// 定义块级元素的正则表达式 (按优先级)
const rules = {
newline: /^\n+/,
code: /^```(\w*)\n([\s\S]+?)\n```\n*/,
fences: /^ {0,3}(`{3,}|~{3,})([^`~\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,
heading: /^ {0,3}(#{1,6}) (.*)(?:\n+|$)/,
hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,
blockquote: /^( {0,3}> ?(.*)(?:\n|$))+/,
list: /^( {0,3}(?:[*+-]|\d+\.) [^\n]*(?:\n(?!(?:[*+-]|\d+\. |>|#|`{3,}|-{3,}))[^\n]*)*)+/i,
html: /^ {0,3}(?:<(script|pre|style|textarea)[\s>][\s\S]*?(?:<\/\1>[^\n]*\n+|$)|<!--[\s\S]*?(?:-->|$)|<\?[\s\S]*?(?:\?>\n*|$)|<![A-Z][\s\S]*?(?:>\n*|$)|<!\[CDATA\[[\s\S]*?(?:\]\]>\n*|$)|<\/?(address|article|aside|base|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|nav|ol|p|param|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\n|\/?>)[\s\S]*?(?:\n\n+|$)|<(?!script|pre|style|textarea)[a-z][\w-]*\s*\/?>(?=[ \t]*(?:\n|$))[\s\S]*?(?:\n\n+|$))/,
setextHeading: /^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,
paragraph: /^([^\n]+(?:\n(?! {0,3}(?:[*+-]|\d+\.) |>|#|`{3,}|-{3,}|={3,})[^\n]+)*)\n*/
};
while (src) {
// 1. 空行
let cap = rules.newline.exec(src);
if (cap) {
src = src.substring(cap[0].length);
tokens.push({ type: 'space' });
continue;
}
// 2. Fenced Code Block (```)
cap = rules.fences.exec(src);
if (cap) {
src = src.substring(cap[0].length);
tokens.push({
type: 'code',
lang: cap[2] ? cap[2].trim() : '',
text: cap[3] || ''
});
continue;
}
// 3. ATX Heading (# h1)
cap = rules.heading.exec(src);
if (cap) {
src = src.substring(cap[0].length);
tokens.push({
type: 'heading',
depth: cap[1].length,
text: cap[2].trim()
});
continue;
}
// 4. Setext Heading (underline)
cap = rules.setextHeading.exec(src);
if(cap) {
src = src.substring(cap[0].length);
tokens.push({
type: 'heading',
depth: cap[2].charAt(0) === '=' ? 1 : 2,
text: cap[1]
});
continue;
}
// 5. Horizontal Rule
cap = rules.hr.exec(src);
if (cap) {
src = src.substring(cap[0].length);
tokens.push({ type: 'hr' });
continue;
}
// 6. Blockquote
cap = rules.blockquote.exec(src);
if (cap) {
src = src.substring(cap[0].length);
// 移除每行开头的 '>' 和一个可选的空格
const bqContent = cap[0].replace(/^ *> ?/gm, '');
tokens.push({
type: 'blockquote',
// 递归地对块引用内容进行词法分析
tokens: tokenize(bqContent)
});
continue;
}
// 7. List
cap = rules.list.exec(src);
if (cap) {
src = src.substring(cap[0].length);
const listStr = cap[0];
const ordered = /^\d+\./.test(listStr);
const itemRegex = /^( *)([*+-]|\d+\.) +([^\n]*(?:\n(?! {0,3}(?:[*+-]|\d+\.) )[^\n]*)*)/gm;
const items = [];
let match;
while ((match = itemRegex.exec(listStr)) !== null) {
const [, indent, , itemContent] = match;
// 处理嵌套内容,移除当前项的缩进
const nestedContent = itemContent.replace(new RegExp('^' + ' '.repeat(indent.length), 'gm'), '');
items.push({
type: 'list_item',
// 递归地对列表项内容进行词法分析
tokens: tokenize(nestedContent)
});
}
tokens.push({
type: 'list',
ordered: ordered,
items: items
});
continue;
}
// 8. Raw HTML
cap = rules.html.exec(src);
if (cap) {
src = src.substring(cap[0].length);
tokens.push({
type: 'html',
text: cap[0]
});
continue;
}
// 9. Paragraph (作为最后的 fallback)
cap = rules.paragraph.exec(src);
if (cap) {
src = src.substring(cap[0].length);
tokens.push({
type: 'paragraph',
text: cap[1].trim()
});
continue;
}
// 如果没有规则匹配,说明有未知语法,跳过一个字符防止死循环
if (src) {
console.error(`Infinite loop on: ${src.slice(0, 20)}`);
src = src.substring(1);
}
}
return tokens;
};
// --- 4. 渲染器 (Renderer) ---
const render = (tokens) => {
let html = '';
for (const token of tokens) {
switch (token.type) {
case 'space':
break;
case 'hr':
html += '<hr>\n';
break;
case 'heading':
html += `<h${token.depth}>${parseInline(token.text)}</h${token.depth}>\n`;
break;
case 'code':
const langClass = token.lang ? ` class="language-${escapeHtml(token.lang)}"` : '';
html += `<pre><code${langClass}>${escapeHtml(token.text)}</code></pre>\n`;
break;
case 'blockquote':
// 递归渲染块引用内的令牌
html += `<blockquote>\n${render(token.tokens)}</blockquote>\n`;
break;
case 'list':
const tag = 'ul';
let listContent = '';
for (const item of token.items) {
// 递归渲染列表项内的令牌
listContent += `<li>${render(item.tokens).trim()}</li>\n`;
}
html += `<${tag}>\n${listContent}</${tag}>\n`;
break;
case 'paragraph':
html += `<p>${parseInline(token.text)}</p>\n`;
break;
case 'html':
html += token.text;
break;
default:
console.error(`Unknown token type: ${token.type}`);
}
}
return html;
};
// --- 执行流程 ---
const tokens = tokenize(preprocessedMarkdown);
const result = render(tokens);
return result.trim();
}

View File

@@ -4,8 +4,10 @@ import { handleGetContent } from './handlers/getContent.js';
import { handleGetContentHtml } from './handlers/getContentHtml.js';
import { handleGenAIContent, handleGenAIPodcastScript, handleGenAIDailyAnalysis } from './handlers/genAIContent.js'; // Import handleGenAIPodcastScript and handleGenAIDailyAnalysis
import { handleCommitToGitHub } from './handlers/commitToGitHub.js';
import { dataSources } from './dataFetchers.js'; // Import dataSources
import { handleLogin, isAuthenticated, handleLogout } from './auth.js'; // Import auth functions
import { handleRss } from './handlers/getRss.js';
import { handleWriteRssData } from './handlers/writeRssData.js';
import { dataSources } from './dataFetchers.js';
import { handleLogin, isAuthenticated, handleLogout } from './auth.js';
export default {
async fetch(request, env) {
@@ -44,6 +46,10 @@ export default {
return await handleLogout(request, env);
} else if (path === '/getContent' && request.method === 'GET') {
return await handleGetContent(request, env);
} else if (path.startsWith('/rss') && request.method === 'GET') {
return await handleRss(request, env);
} else if (path === '/writeRssData' && request.method === 'GET') {
return await handleWriteRssData(request, env);
}
// Authentication check for all other paths
@@ -75,7 +81,7 @@ export default {
response = await handleGenAIDailyAnalysis(request, env);
} else if (path === '/commitToGitHub' && request.method === 'POST') {
response = await handleCommitToGitHub(request, env);
} else {
} else {
return new Response(null, { status: 404, headers: {'Content-Type': 'text/plain; charset=utf-8'} });
}
} catch (e) {

2189
src/marked.esm.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -39,4 +39,6 @@ LOGIN_PASSWORD = "toor"
DAILY_TITLE = "AI洞察日报"
PODCAST_TITLE = "来生小酒馆"
PODCAST_BEGIN = "嘿亲爱的V欢迎收听新一期的来生情报站我是你们的老朋友何夕2077"
PODCAST_END = "今天的情报就到这里,注意隐蔽,赶紧撤离"
PODCAST_END = "今天的情报就到这里,注意隐蔽,赶紧撤离"
BOOK_LINK = ""
INSERT_FOOT = "false"