📄 friendly_file_server.js

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');

const PORT = 8081;
const DIRECTORY = '/data/ai-output';

// Markdown转HTML的简单实现
function markdownToHtml(markdown) {
    let html = markdown
        // 转义HTML
        .replace(/&/g, '&')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        // 标题
        .replace(/^### (.*$)/gim, '<h3>$1</h3>')
        .replace(/^## (.*$)/gim, '<h2>$1</h2>')
        .replace(/^# (.*$)/gim, '<h1>$1</h1>')
        // 粗体和斜体
        .replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/\*(.*?)\*/g, '<em>$1</em>')
        // 代码
        .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
        .replace(/`(.*?)`/g, '<code>$1</code>')
        // 链接
        .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
        // 列表
        .replace(/^\- \[ \] (.*$)/gim, '<li style="list-style: none;">☐ $1</li>')
        .replace(/^\- \[x\] (.*$)/gim, '<li style="list-style: none;">☑ $1</li>')
        .replace(/^\- (.*$)/gim, '<li>$1</li>')
        .replace(/^\d+\. (.*$)/gim, '<li>$1</li>')
        // 引用
        .replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>')
        // 水平线
        .replace(/^---$/gim, '<hr>')
        // 换行
        .replace(/\n\n/g, '</p><p>')
        .replace(/\n/g, '<br>');
    
    return `<p>${html}</p>`;
}

// 生成HTML页面
function generatePage(content, filename) {
    return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${filename}</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
            line-height: 1.8;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .container {
            max-width: 1000px;
            margin: 0 auto;
            background: white;
            border-radius: 12px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .header h1 {
            font-size: 24px;
            font-weight: 600;
        }
        .toolbar {
            display: flex;
            gap: 10px;
        }
        .btn {
            display: inline-block;
            padding: 10px 20px;
            background: white;
            color: #667eea;
            text-decoration: none;
            border-radius: 6px;
            font-weight: 500;
            transition: all 0.3s;
        }
        .btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
        }
        .btn-primary {
            background: #00C853;
            color: white;
        }
        .content {
            padding: 40px;
            color: #333;
        }
        h1 { 
            color: #2c3e50; 
            border-bottom: 3px solid #667eea; 
            padding-bottom: 15px; 
            margin: 30px 0 20px 0;
            font-size: 32px;
        }
        h2 { 
            color: #34495e; 
            border-bottom: 2px solid #95a5a6; 
            padding-bottom: 10px; 
            margin: 25px 0 15px 0;
            font-size: 26px;
        }
        h3 { 
            color: #7f8c8d; 
            margin: 20px 0 10px 0;
            font-size: 22px;
        }
        h4 {
            color: #95a5a6;
            margin: 15px 0 10px 0;
            font-size: 18px;
        }
        p { margin: 15px 0; }
        table { 
            border-collapse: collapse; 
            width: 100%; 
            margin: 20px 0; 
            font-size: 14px;
        }
        th, td { 
            border: 1px solid #e0e0e0; 
            padding: 12px 15px; 
            text-align: left; 
        }
        th { 
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white; 
            font-weight: 600;
        }
        tr:nth-child(even) { background-color: #f9f9f9; }
        tr:hover { background-color: #f0f0ff; }
        code { 
            background: #f4f4f4; 
            padding: 3px 8px; 
            border-radius: 4px; 
            font-size: 0.9em;
            font-family: 'Courier New', monospace;
            color: #e74c3c;
        }
        pre { 
            background: #2c3e50; 
            color: #ecf0f1; 
            padding: 20px; 
            border-radius: 8px; 
            overflow-x: auto;
            margin: 20px 0;
        }
        pre code { 
            background: none; 
            padding: 0; 
            color: #ecf0f1;
        }
        blockquote { 
            border-left: 5px solid #667eea; 
            padding: 15px 20px; 
            margin: 20px 0; 
            color: #7f8c8d;
            background: #f9f9f9;
            border-radius: 0 8px 8px 0;
        }
        a { 
            color: #667eea; 
            text-decoration: none; 
            border-bottom: 1px dashed #667eea;
        }
        a:hover { 
            color: #764ba2;
            border-bottom-style: solid;
        }
        hr {
            border: none;
            height: 2px;
            background: linear-gradient(to right, transparent, #667eea, transparent);
            margin: 30px 0;
        }
        li {
            margin: 8px 0;
            line-height: 1.6;
        }
        ul, ol {
            padding-left: 30px;
            margin: 15px 0;
        }
        .footer {
            text-align: center;
            padding: 20px;
            background: #f5f5f5;
            color: #999;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📄 ${filename}</h1>
            <div class="toolbar">
                <a href="?raw=1" class="btn">📝 查看源文件</a>
                <a href="${filename}" download class="btn btn-primary">⬇️ 下载文件</a>
            </div>
        </div>
        <div class="content">
            ${content}
        </div>
        <div class="footer">
            Generated by Enhanced File Server • ${new Date().toLocaleString('zh-CN')}
        </div>
    </div>
</body>
</html>`;
}

const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    const pathname = decodeURIComponent(parsedUrl.pathname);
    const filePath = path.join(DIRECTORY, pathname);
    
    console.log(`[${new Date().toLocaleString()}] ${req.method} ${pathname}`);
    
    // 检查文件是否存在
    if (!fs.existsSync(filePath)) {
        res.writeHead(404, {'Content-Type': 'text/html; charset=utf-8'});
        res.end('<h1>404 - 文件未找到</h1>');
        return;
    }
    
    const stat = fs.statSync(filePath);
    
    // 如果是目录,列出文件
    if (stat.isDirectory()) {
        const files = fs.readdirSync(filePath);
        const list = files.map(f => {
            const fstat = fs.statSync(path.join(filePath, f));
            const icon = fstat.isDirectory() ? '📁' : '📄';
            return `<li><a href="${path.join(pathname, f)}">${icon} ${f}</a></li>`;
        }).join('');
        
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.end(generatePage(`<h2>目录列表</h2><ul>${list}</ul>`, '目录'));
        return;
    }
    
    // 读取文件
    const content = fs.readFileSync(filePath, 'utf-8');
    const ext = path.extname(filePath).toLowerCase();
    
    // 如果是Markdown且没有raw参数,渲染为HTML
    if (ext === '.md' && !parsedUrl.query.raw) {
        const htmlContent = markdownToHtml(content);
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.end(generatePage(htmlContent, path.basename(filePath)));
    } else {
        // 其他文件或raw模式,直接返回
        const mimeTypes = {
            '.md': 'text/markdown; charset=utf-8',
            '.txt': 'text/plain; charset=utf-8',
            '.html': 'text/html; charset=utf-8',
            '.css': 'text/css; charset=utf-8',
            '.js': 'application/javascript; charset=utf-8',
            '.json': 'application/json; charset=utf-8',
            '.png': 'image/png',
            '.jpg': 'image/jpeg',
            '.jpeg': 'image/jpeg',
            '.gif': 'image/gif',
            '.pdf': 'application/pdf',
            '.zip': 'application/zip'
        };
        
        const mimeType = mimeTypes[ext] || 'application/octet-stream';
        res.writeHead(200, {
            'Content-Type': mimeType,
            'Content-Length': stat.size,
            'Access-Control-Allow-Origin': '*'
        });
        res.end(content);
    }
});

server.listen(PORT, () => {
    console.log(`✅ Enhanced File Server运行中`);
    console.log(`📁 服务目录: ${DIRECTORY}`);
    console.log(`🌐 访问地址: http://proxy.mossai.icu:${PORT}`);
    console.log(`🎨 功能: Markdown渲染、UTF-8编码、美观界面`);
});