From 5dbbe8f48499f6fade2dcc26c7760dd23e01a9fa Mon Sep 17 00:00:00 2001 From: justlovemaki <274166795@qq.com> Date: Thu, 12 Jun 2025 17:41:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=8A=9F=E8=83=BD=E5=B9=B6=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在wrangler.toml中添加IMG_PROXY配置项用于图片代理 - 新增replaceImageProxy函数处理图片链接替换 - 实现KV存储的会话管理功能 - 在生成AI内容页面添加图片代理和新窗口预览功能 - 完善Markdown转HTML功能,支持更多语法元素 --- src/auth.js | 16 +- src/handlers/genAIContent.js | 20 +-- src/helpers.js | 5 + src/htmlGenerators.js | 306 ++++++++++++++++++++++++++++++++++- wrangler.toml | 1 + 5 files changed, 330 insertions(+), 18 deletions(-) diff --git a/src/auth.js b/src/auth.js index 87748d2..3bc2ae5 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,4 +1,6 @@ // src/auth.js +import { storeInKV, getFromKV} from './kv.js'; + const SESSION_COOKIE_NAME = 'session_id_89757'; const SESSION_EXPIRATION_SECONDS = 60 * 60; // 1 hour @@ -95,7 +97,7 @@ async function handleLogin(request, env) { const sessionId = crypto.randomUUID(); // Generate a simple session ID // Store sessionId in KV store for persistent sessions - // await env.DATA_KV.put(`session:${sessionId}`, 'valid', { expirationTtl: SESSION_EXPIRATION_SECONDS }); + await storeInKV(env.DATA_KV, `session:${sessionId}`, 'valid', SESSION_EXPIRATION_SECONDS); const cookie = setSessionCookie(sessionId); @@ -130,11 +132,13 @@ async function isAuthenticated(request, env) { const sessionId = sessionCookie.split('=')[1]; // Validate sessionId against KV store - // const storedSession = await env.DATA_KV.get(`session:${sessionId}`); - // if (storedSession !== 'valid') { - // return { authenticated: false, cookie: null }; - // } + const storedSession = await getFromKV(env.DATA_KV, `session:${sessionId}`); + if (storedSession !== 'valid') { + return { authenticated: false, cookie: null }; + } + // Store sessionId in KV store for persistent sessions + await storeInKV(env.DATA_KV, `session:${sessionId}`, 'valid', SESSION_EXPIRATION_SECONDS); // Renew the session cookie const newCookie = setSessionCookie(sessionId); return { authenticated: true, cookie: newCookie }; @@ -149,7 +153,7 @@ async function handleLogout(request, env) { if (sessionCookie) { const sessionId = sessionCookie.split('=')[1]; // Delete session from KV store - // await env.DATA_KV.delete(`session:${sessionId}`); + await env.DATA_KV.delete(`session:${sessionId}`); } } diff --git a/src/handlers/genAIContent.js b/src/handlers/genAIContent.js index 61fee48..a6a7e5a 100644 --- a/src/handlers/genAIContent.js +++ b/src/handlers/genAIContent.js @@ -27,7 +27,7 @@ export async function handleGenAIPodcastScript(request, env) { outputOfCall1 = formData.get('summarizedContent'); // Get summarized content from form data if (!outputOfCall1) { - const errorHtml = generateGenAiPageHtml('生成AI播客脚本出错', '

Summarized content is missing. Please go back and generate AI content first.

', dateStr, true, null); + const errorHtml = generateGenAiPageHtml(env, '生成AI播客脚本出错', '

Summarized content is missing. Please go back and generate AI content first.

', dateStr, true, null); return new Response(errorHtml, { status: 400, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } @@ -47,7 +47,7 @@ export async function handleGenAIPodcastScript(request, env) { console.log("Call 2 (Podcast Formatting) successful. Final output length:", finalAiResponse.length); } catch (error) { console.error("Error in Chat API Call 2 (Podcast Formatting):", error); - const errorHtml = generateGenAiPageHtml('生成AI播客脚本出错(播客文案)', `

Failed during podcast formatting: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, dateStr, true, selectedItemsParams, null, null, fullPromptForCall2_System, fullPromptForCall2_User); + const errorHtml = generateGenAiPageHtml(env, '生成AI播客脚本出错(播客文案)', `

Failed during podcast formatting: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, dateStr, true, selectedItemsParams, null, null, fullPromptForCall2_System, fullPromptForCall2_User); return new Response(errorHtml, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } @@ -59,6 +59,7 @@ export async function handleGenAIPodcastScript(request, env) { let podcastScriptMarkdownContent = `# ${env.PODCAST_TITLE} ${formatDateToChinese(dateStr)}\n\n${removeMarkdownCodeBlock(finalAiResponse)}`; const successHtml = generateGenAiPageHtml( + env, 'AI播客脚本', escapeHtml(finalAiResponse), dateStr, false, selectedItemsParams, @@ -74,7 +75,7 @@ export async function handleGenAIPodcastScript(request, env) { console.error("Error in /genAIPodcastScript (outer try-catch):", error); const pageDateForError = dateStr || getISODate(); const itemsForActionOnError = Array.isArray(selectedItemsParams) ? selectedItemsParams : []; - const errorHtml = generateGenAiPageHtml('生成AI播客脚本出错', `

Unexpected error: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, pageDateForError, true, itemsForActionOnError, null, null, fullPromptForCall2_System, fullPromptForCall2_User); + const errorHtml = generateGenAiPageHtml(env, '生成AI播客脚本出错', `

Unexpected error: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, pageDateForError, true, itemsForActionOnError, null, null, fullPromptForCall2_System, fullPromptForCall2_User); return new Response(errorHtml, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } } @@ -96,7 +97,7 @@ export async function handleGenAIContent(request, env) { selectedItemsParams = formData.getAll('selectedItems'); if (selectedItemsParams.length === 0) { - const errorHtml = generateGenAiPageHtml('生成AI日报出错,未选生成条目', '

No items were selected. Please go back and select at least one item.

', dateStr, true, null); + const errorHtml = generateGenAiPageHtml(env, '生成AI日报出错,未选生成条目', '

No items were selected. Please go back and select at least one item.

', dateStr, true, null); return new Response(errorHtml, { status: 400, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } @@ -159,13 +160,13 @@ export async function handleGenAIContent(request, env) { } if (validItemsProcessedCount === 0) { - const errorHtml = generateGenAiPageHtml('生成AI日报出错,可生成条目为空', '

Selected items could not be retrieved or resulted in no content. Please check the data or try different selections.

', dateStr, true, selectedItemsParams); + const errorHtml = generateGenAiPageHtml(env, '生成AI日报出错,可生成条目为空', '

Selected items could not be retrieved or resulted in no content. Please check the data or try different selections.

', dateStr, true, selectedItemsParams); return new Response(errorHtml, { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } //提示词内不能有英文引号,否则会存储数据缺失。 fullPromptForCall1_System = getSystemPromptSummarizationStepOne(); - fullPromptForCall1_User = selectedContentItems.join('\n\n---\n\n'); // Keep this for logging/error reporting if needed + fullPromptForCall1_User = '\n\n------\n\n'+selectedContentItems.join('\n\n------\n\n')+'\n\n------\n\n'; // Keep this for logging/error reporting if needed console.log("Call 1 to Chat (Summarization): User prompt length:", fullPromptForCall1_User.length); try { @@ -193,7 +194,7 @@ export async function handleGenAIContent(request, env) { console.log("Call 1 (Summarization) successful. Output length:", outputOfCall1.length); } catch (error) { console.error("Error in Chat API Call 1 (Summarization):", error); - const errorHtml = generateGenAiPageHtml('生成AI日报出错(分段处理)', `

Failed during summarization: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, dateStr, true, selectedItemsParams, fullPromptForCall1_System, fullPromptForCall1_User); + const errorHtml = generateGenAiPageHtml(env, '生成AI日报出错(分段处理)', `

Failed during summarization: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, dateStr, true, selectedItemsParams, fullPromptForCall1_System, fullPromptForCall1_User); return new Response(errorHtml, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } @@ -214,7 +215,7 @@ export async function handleGenAIContent(request, env) { console.log("Call 2 (Processing Call 1 Output) successful. Output length:", outputOfCall2.length); } catch (error) { console.error("Error in Chat API Call 2 (Processing Call 1 Output):", error); - const errorHtml = generateGenAiPageHtml('生成AI日报出错(格式化)', `

Failed during processing of summarized content: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, dateStr, true, selectedItemsParams, fullPromptForCall1_System, fullPromptForCall1_User, fullPromptForCall2_System, fullPromptForCall2_User); + const errorHtml = generateGenAiPageHtml(env, '生成AI日报出错(格式化)', `

Failed during processing of summarized content: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, dateStr, true, selectedItemsParams, fullPromptForCall1_System, fullPromptForCall1_User, fullPromptForCall2_System, fullPromptForCall2_User); return new Response(errorHtml, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } @@ -229,6 +230,7 @@ export async function handleGenAIContent(request, env) { let dailySummaryMarkdownContent = `# ${env.DAILY_TITLE} ${formatDateToChinese(dateStr)}\n\n${removeMarkdownCodeBlock(outputOfCall2)}`; const successHtml = generateGenAiPageHtml( + env, 'AI日报', // Title for Call 1 page escapeHtml(outputOfCall2), dateStr, false, selectedItemsParams, @@ -245,7 +247,7 @@ export async function handleGenAIContent(request, env) { console.error("Error in /genAIContent (outer try-catch):", error); const pageDateForError = dateStr || getISODate(); const itemsForActionOnError = Array.isArray(selectedItemsParams) ? selectedItemsParams : []; - const errorHtml = generateGenAiPageHtml('生成AI日报出错', `

Unexpected error: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, pageDateForError, true, itemsForActionOnError, fullPromptForCall1_System, fullPromptForCall1_User, fullPromptForCall2_System, fullPromptForCall2_User); + const errorHtml = generateGenAiPageHtml(env, '生成AI日报出错', `

Unexpected error: ${escapeHtml(error.message)}

${error.stack ? `
${escapeHtml(error.stack)}
` : ''}`, pageDateForError, true, itemsForActionOnError, fullPromptForCall1_System, fullPromptForCall1_User, fullPromptForCall2_System, fullPromptForCall2_User); return new Response(errorHtml, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } } diff --git a/src/helpers.js b/src/helpers.js index 0215d6c..bfd431e 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -244,3 +244,8 @@ export function getRandomUserAgent() { export function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } + +export function replaceImageProxy(proxy, content) { + const str = String(content); + return str.replace(/upload.chinaz.com/g, 'pic.chinaz.com').replace(/https:\/\/pic.chinaz.com/g, proxy+'https:\/\/pic.chinaz.com'); +} \ No newline at end of file diff --git a/src/htmlGenerators.js b/src/htmlGenerators.js index 090c52e..f9ee440 100644 --- a/src/htmlGenerators.js +++ b/src/htmlGenerators.js @@ -1,5 +1,5 @@ // src/htmlGenerators.js -import { escapeHtml, formatDateToChinese, convertEnglishQuotesToChinese} from './helpers.js'; +import { escapeHtml, formatDateToChinese, convertEnglishQuotesToChinese, replaceImageProxy} from './helpers.js'; import { dataSources } from './dataFetchers.js'; // Import dataSources function generateHtmlListForContentPage(items, dateStr) { @@ -275,7 +275,8 @@ function generatePromptSectionHtmlForGenAI(systemPrompt, userPrompt, promptTitle `; } -export function generateGenAiPageHtml(title, bodyContent, pageDate, isErrorPage = false, selectedItemsForAction = null, + +export function generateGenAiPageHtml(env, title, bodyContent, pageDate, isErrorPage = false, selectedItemsForAction = null, systemP1 = null, userP1 = null, systemP2 = null, userP2 = null, promptsMd = null, dailyMd = null, podcastMd = null) { @@ -303,6 +304,7 @@ export function generateGenAiPageHtml(title, bodyContent, pageDate, isErrorPage let githubSaveFormHtml = ''; let generatePodcastButtonHtml = ''; let aiDailyAnalysisButtonHtml = ''; + let outDisplayButtonHtml = ''; // Since commitToGitHub and genAIPodcastScript are now API calls, // these forms should be handled by JavaScript on the client side. @@ -334,6 +336,9 @@ export function generateGenAiPageHtml(title, bodyContent, pageDate, isErrorPage `; + outDisplayButtonHtml = ` + + `; } let promptDisplayHtml = ''; @@ -377,6 +382,7 @@ export function generateGenAiPageHtml(title, bodyContent, pageDate, isErrorPage .toggle-prompt-btn:hover { background-color: #5a6268; } .copy-prompt-btn { background-color: #17a2b8; font-size: 0.85rem; padding: 0.4rem 0.8rem;} .copy-prompt-btn:hover { background-color: #138496;} + #outContentBox { display: none;}
@@ -384,10 +390,12 @@ export function generateGenAiPageHtml(title, bodyContent, pageDate, isErrorPage
${generatePodcastButtonHtml} ${aiDailyAnalysisButtonHtml} + ${outDisplayButtonHtml}

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

-
${bodyContent}
+
${bodyContent}
+
${markdownToHtml(replaceImageProxy(env.IMG_PROXY, bodyContent))}
${promptDisplayHtml}
`; } + + +/** + * 一个功能完善的 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 `${escapeHtml(alt)}`; + }); + + // 链接: [text](href "title") + html = html.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_, text, href, title) => { + let titleAttr = title ? ` title="${escapeHtml(title)}"` : ''; + return `${parseInline(text)}`; // 递归处理链接文本中的内联格式 + }); + + // 行内代码: `code` + html = html.replace(/`([^`]+)`/g, (_, code) => `${escapeHtml(code)}`); + + // 加粗+斜体: ***text*** 或 ___text___ + html = html.replace(/(\*\*\*|___)(.+?)\1/g, '$2'); + + // 加粗: **text** 或 __text__ + html = html.replace(/(\*\*|__)(.+?)\1/g, '$2'); + + // 斜体: *text* 或 _text_ + // html = html.replace(/(\*|_)(.+?)\1/g, '$2'); + + // 删除线: ~~text~~ + html = html.replace(/~~(.+?)~~/g, '$1'); + + // 换行: 行尾两个空格 + html = html.replace(/ {2,}\n/g, '
\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]*?(?:\?>\n*|$)|\n*|$)|\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 += '
\n'; + break; + case 'heading': + html += `${parseInline(token.text)}\n`; + break; + + case 'code': + const langClass = token.lang ? ` class="language-${escapeHtml(token.lang)}"` : ''; + html += `
${escapeHtml(token.text)}
\n`; + break; + + case 'blockquote': + // 递归渲染块引用内的令牌 + html += `
\n${render(token.tokens)}
\n`; + break; + + case 'list': + const tag = 'ul'; + let listContent = ''; + for (const item of token.items) { + // 递归渲染列表项内的令牌 + listContent += `
  • ${render(item.tokens).trim()}
  • \n`; + } + html += `<${tag}>\n${listContent}\n`; + break; + + case 'paragraph': + html += `

    ${parseInline(token.text)}

    \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(); +} diff --git a/wrangler.toml b/wrangler.toml index a57e62d..3341aaf 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -9,6 +9,7 @@ kv_namespaces = [ ] [vars] +IMG_PROXY = "" #图片代理链接,用于处理图片不显示 OPEN_TRANSLATE = "true" USE_MODEL_PLATFORM = "GEMINI" #GEMINI, OPEN GEMINI_API_KEY = "xxxxxx-xxxxxx"