import fs from 'node:fs/promises'; import path from 'node:path'; import { PUBLISH_ROOT_DIR } from './user-publish.mjs'; const URL_PATTERN = /https?:\/\/[^\s<>"')\]]+\/(?:MindSpace|temp)\/([a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi; function decodePathSegment(segment) { try { return decodeURIComponent(segment); } catch { return segment; } } export function extractStaticPageLinks(content, { username } = {}) { const text = String(content ?? ''); const links = []; const seen = new Set(); for (const match of text.matchAll(URL_PATTERN)) { const owner = decodePathSegment(match[1]).toLowerCase(); const relativePath = decodePathSegment(match[2]); if (username && owner !== String(username).trim().toLowerCase()) continue; const key = `${owner}/${relativePath}`; if (seen.has(key)) continue; seen.add(key); links.push({ publicUrl: match[0], owner, relativePath, filename: path.basename(relativePath), }); } return links; } export function resolvePublishHtmlAbsolutePath(h5Root, username, relativePath) { const slug = String(username ?? '').trim().toLowerCase(); const clean = String(relativePath ?? '') .replace(/^\/+/, '') .split('/') .filter((part) => part && part !== '.' && part !== '..') .join('/'); if (!slug || !clean || !clean.toLowerCase().endsWith('.html')) { throw Object.assign(new Error('无效的页面路径'), { code: 'invalid_page_path' }); } const publishRoot = path.resolve(h5Root, PUBLISH_ROOT_DIR, slug); const absolute = path.resolve(publishRoot, clean); if (absolute !== publishRoot && !absolute.startsWith(`${publishRoot}${path.sep}`)) { throw Object.assign(new Error('页面路径越界'), { code: 'invalid_page_path' }); } return absolute; } export async function readPublishHtml(h5Root, username, relativePath) { const absolute = resolvePublishHtmlAbsolutePath(h5Root, username, relativePath); const content = await fs.readFile(absolute, 'utf8'); if (!content.trim()) { throw Object.assign(new Error('页面内容为空'), { code: 'empty_page_content' }); } return { absolute, content, relativePath, filename: path.basename(relativePath) }; } function titleFromHtml(html) { const match = String(html).match(/]*>([^<]+)<\/title>/i); return match?.[1]?.trim() ?? ''; } function summaryFromHtml(html) { const stripped = String(html) .replace(//gi, ' ') .replace(//gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim(); return stripped.slice(0, 180); } export function analyzeChatMessageForSave({ content, username, h5Root, selectedLinkIndex = 0, }) { const links = extractStaticPageLinks(content, { username }); const text = String(content ?? '').replace(/\s+/g, ' ').trim(); const suggestedTitleFromText = text .replace(/^#{1,6}\s*/, '') .replace(/[*_`~[\]]/g, '') .trim() .slice(0, 48); if (links.length === 0) { return { contentMode: 'markdown', links: [], selectedLink: null, suggestedTitle: suggestedTitleFromText || 'AI 创作页面', suggestedSummary: text.slice(0, 160), previewUrl: null, relativePath: null, filename: null, }; } const index = Math.min(Math.max(0, selectedLinkIndex), links.length - 1); const selectedLink = links[index]; return { contentMode: 'static_html', links, selectedLink, suggestedTitle: selectedLink.filename.replace(/\.html$/i, '').replace(/[-_]/g, ' ') || suggestedTitleFromText || 'AI 生成页面', suggestedSummary: text.slice(0, 160), previewUrl: selectedLink.publicUrl, relativePath: selectedLink.relativePath, filename: selectedLink.filename, h5Root, username, }; } export async function resolveStaticHtmlContent(analysis) { if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) { return null; } const loaded = await readPublishHtml( analysis.h5Root, analysis.username, analysis.selectedLink.relativePath, ); const title = titleFromHtml(loaded.content); const summary = summaryFromHtml(loaded.content); return { ...loaded, suggestedTitle: title || analysis.suggestedTitle, suggestedSummary: summary || analysis.suggestedSummary, publicUrl: analysis.selectedLink.publicUrl, }; }