📄 file_server_dynamic.js

const express = require('express');
const Database = require('better-sqlite3');
const marked = require('marked');
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = 8082;  // 新端口,避免冲突
const DIRECTORY = '/data/ai-output';

// 初始化SQLite数据库
const db = new Database('/data/ai-output/file_server.db');

// 创建表
db.exec(`
  CREATE TABLE IF NOT EXISTS todo_items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    file_path TEXT NOT NULL,
    item_index INTEGER NOT NULL,
    checked INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(file_path, item_index)
  );
  
  CREATE TABLE IF NOT EXISTS fold_states (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    file_path TEXT NOT NULL,
    heading_id TEXT NOT NULL,
    collapsed INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(file_path, heading_id)
  );
`);

// 中间件
app.use(express.json());
app.use(express.static('public'));

// CORS支持
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

// 自定义Markdown渲染器
class CustomRenderer extends marked.Renderer {
  constructor(options) {
    super(options);
    this.todoIndex = 0;
    this.headings = [];
    this.currentFilePath = options.filePath || '';
  }

  // 待办列表渲染
  listitem(text) {
    if (text.startsWith('<input')) {
      return `<li class="todo-item">${text}</li>\n`;
    }
    return super.listitem(text);
  }

  // 标题渲染(支持折叠)
  heading(text, level, raw, slugger) {
    const id = slugger.slug(raw);
    this.headings.push({ level, text: raw, id });
    
    const foldState = db.prepare(`
      SELECT collapsed FROM fold_states 
      WHERE file_path = ? AND heading_id = ?
    `).get(this.currentFilePath, id);
    
    const isCollapsed = foldState ? foldState.collapsed : 0;
    const collapseIcon = isCollapsed ? '▶' : '▼';
    
    return `
      <div class="heading-wrapper" data-heading-id="${id}">
        <button class="fold-btn" onclick="toggleFold('${id}')">${collapseIcon}</button>
        <h${level} id="${id}" class="${isCollapsed ? 'collapsed' : ''}">${text}</h${level}>
      </div>
    `;
  }
}

// API: 更新待办项状态
app.post('/api/todo/toggle', (req, res) => {
  const { filePath, itemIndex, checked } = req.body;
  
  try {
    const stmt = db.prepare(`
      INSERT INTO todo_items (file_path, item_index, checked, updated_at)
      VALUES (?, ?, ?, datetime('now'))
      ON CONFLICT(file_path, item_index) 
      DO UPDATE SET checked = ?, updated_at = datetime('now')
    `);
    
    stmt.run(filePath, itemIndex, checked ? 1 : 0, checked ? 1 : 0);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// API: 更新折叠状态
app.post('/api/fold/toggle', (req, res) => {
  const { filePath, headingId, collapsed } = req.body;
  
  try {
    const stmt = db.prepare(`
      INSERT INTO fold_states (file_path, heading_id, collapsed, updated_at)
      VALUES (?, ?, ?, datetime('now'))
      ON CONFLICT(file_path, heading_id) 
      DO UPDATE SET collapsed = ?, updated_at = datetime('now')
    `);
    
    stmt.run(filePath, headingId, collapsed ? 1 : 0, collapsed ? 1 : 0);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// API: 获取文档大纲
app.get('/api/outline', (req, res) => {
  const { filePath } = req.query;
  
  try {
    const content = fs.readFileSync(path.join(DIRECTORY, filePath), 'utf-8');
    const renderer = new CustomRenderer({ filePath });
    marked.use({ renderer });
    marked.parse(content);
    
    res.json({ success: true, outline: renderer.headings });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// Markdown渲染页面 - 使用正则匹配所有路径
app.get(/^\/view\/(.*)/, (req, res) => {
  const filePath = decodeURIComponent(req.params[0]);
  const fullPath = path.join(DIRECTORY, filePath);
  
  if (!fs.existsSync(fullPath)) {
    return res.status(404).send('文件未找到: ' + filePath);
  }
  
  const content = fs.readFileSync(fullPath, 'utf-8');
  
  // 读取待办项状态
  const todoStates = db.prepare(`
    SELECT item_index, checked FROM todo_items WHERE file_path = ?
  `).all(filePath);
  
  const todoMap = new Map(todoStates.map(t => [t.item_index, t.checked]));
  
  // 预处理待办列表 - 在Markdown解析前替换
  let processedContent = content;
  let todoIndex = 0;
  processedContent = processedContent.replace(/^- \[([ xX])\] (.*)$/gm, (match, checked, text) => {
    const isChecked = todoMap.get(todoIndex) || checked.toLowerCase() === 'x';
    const checkbox = `<li class="todo-item"><input type="checkbox" class="todo-checkbox" data-index="${todoIndex}" data-file="${filePath}" ${isChecked ? 'checked' : ''}><span>${text}</span></li>`;
    todoIndex++;
    return checkbox;
  });
  
  // 移除markdown中可能存在的script标签(安全过滤)
  processedContent = processedContent.replace(/<script[\s\S]*?<\/script>/gi, '');
  
  // 使用标准marked渲染
  const htmlContent = marked.parse(processedContent);
  
  res.send(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${path.basename(filePath)}</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
      line-height: 1.8;
      background: #f5f5f5;
      color: #333;
      display: flex;
    }
    
    /* 大纲侧边栏 */
    .outline-sidebar {
      width: 250px;
      background: white;
      height: 100vh;
      position: sticky;
      top: 0;
      border-right: 1px solid #e0e0e0;
      padding: 20px;
      overflow-y: auto;
    }
    
    .outline-title {
      font-size: 16px;
      font-weight: bold;
      margin-bottom: 15px;
      color: #666;
    }
    
    .outline-item {
      padding: 8px 12px;
      cursor: pointer;
      border-radius: 6px;
      transition: all 0.2s;
      font-size: 14px;
      color: #555;
    }
    
    .outline-item:hover {
      background: #f0f0f0;
      color: #007bff;
    }
    
    .outline-item.level-1 { padding-left: 12px; font-weight: bold; }
    .outline-item.level-2 { padding-left: 24px; }
    .outline-item.level-3 { padding-left: 36px; font-size: 13px; }
    .outline-item.level-4 { padding-left: 48px; font-size: 13px; }
    
    /* 主内容区 */
    .content-wrapper {
      flex: 1;
      max-width: 900px;
      margin: 0 auto;
      padding: 40px;
    }
    
    .markdown-body {
      background: white;
      padding: 50px;
      border-radius: 12px;
      box-shadow: 0 2px 12px rgba(0,0,0,0.08);
    }
    
    /* 标题样式 */
    .heading-wrapper {
      position: relative;
      margin: 30px 0 15px 0;
    }
    
    .fold-btn {
      position: absolute;
      left: -30px;
      top: 50%;
      transform: translateY(-50%);
      background: none;
      border: none;
      cursor: pointer;
      font-size: 12px;
      color: #999;
      padding: 5px;
      transition: all 0.2s;
    }
    
    .fold-btn:hover {
      color: #007bff;
      transform: translateY(-50%) scale(1.2);
    }
    
    h1, h2, h3, h4, h5, h6 {
      margin: 0 !important;
      color: #2c3e50;
    }
    
    h1 { font-size: 32px; border-bottom: 2px solid #e0e0e0; padding-bottom: 15px; }
    h2 { font-size: 28px; }
    h3 { font-size: 24px; }
    h4 { font-size: 20px; }
    
    /* 待办列表样式 - 优化版 */
    .todo-item {
      list-style: none;
      margin: 12px 0;
      padding: 12px 16px;
      background: #f8f9fa;
      border-radius: 8px;
      border-left: 3px solid #007bff;
      transition: all 0.3s;
    }
    
    .todo-item:hover {
      background: #e9ecef;
      transform: translateX(5px);
    }
    
    .todo-checkbox {
      width: 20px;
      height: 20px;
      cursor: pointer;
      margin-right: 12px;
      vertical-align: middle;
      accent-color: #007bff;
    }
    
    /* 已完成样式 */
    .todo-checkbox:checked + span {
      text-decoration: line-through;
      color: #6c757d;
    }
    
    /* 段落样式 */
    p {
      margin: 15px 0;
      line-height: 1.8;
    }
    
    code {
      background: #f4f4f4;
      padding: 2px 6px;
      border-radius: 4px;
      font-family: 'Courier New', monospace;
    }
    
    pre {
      background: #2d2d2d;
      color: #f8f8f2;
      padding: 20px;
      border-radius: 8px;
      overflow-x: auto;
      margin: 20px 0;
    }
    
    blockquote {
      border-left: 4px solid #007bff;
      padding-left: 20px;
      margin: 20px 0;
      color: #666;
      font-style: italic;
    }
    
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 20px 0;
    }
    
    th, td {
      border: 1px solid #ddd;
      padding: 12px;
      text-align: left;
    }
    
    th {
      background: #007bff;
      color: white;
    }
    
    /* 移动端适配 */
    @media (max-width: 768px) {
      .outline-sidebar {
        display: none;
      }
      
      .content-wrapper {
        padding: 20px;
      }
      
      .markdown-body {
        padding: 25px;
      }
      
      .fold-btn {
        left: -20px;
      }
    }
  </style>
</head>
<body>
  <!-- 大纲侧边栏 -->
  <div class="outline-sidebar">
    <div class="outline-title">📑 文档大纲</div>
    <div id="outline-content">加载中...</div>
  </div>
  
  <!-- 主内容 -->
  <div class="content-wrapper">
    <div class="markdown-body">
      ${htmlContent}
    </div>
  </div>
  
  <script>
    // 使用事件委托处理所有checkbox点击(社区最佳实践)
    document.addEventListener('DOMContentLoaded', function() {
      console.log('✅ 页面加载完成');
      
      // 事件委托 - 监听整个文档的点击
      document.addEventListener('change', function(e) {
        // 检查是否是todo-checkbox
        if (e.target.classList.contains('todo-checkbox')) {
          const checkbox = e.target;
          const index = parseInt(checkbox.getAttribute('data-index'));
          const filePath = checkbox.getAttribute('data-file');
          const checked = checkbox.checked;
          
          console.log('🖱️ Checkbox被点击:', {filePath, index, checked});
          
          // 立即更新视觉反馈
          const span = checkbox.nextElementSibling;
          if (span && span.tagName === 'SPAN') {
            if (checked) {
              span.style.textDecoration = 'line-through';
              span.style.color = '#6c757d';
            } else {
              span.style.textDecoration = 'none';
              span.style.color = '';
            }
          }
          
          // 异步保存到服务器
          fetch('/api/todo/toggle', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
              filePath: filePath,
              itemIndex: index,
              checked: checked
            })
          })
          .then(response => response.json())
          .then(data => {
            console.log('✅ 保存成功:', data);
          })
          .catch(error => {
            console.error('❌ 保存失败:', error);
            // 失败时恢复状态
            checkbox.checked = !checked;
            if (span && span.tagName === 'SPAN') {
              span.style.textDecoration = checked ? 'line-through' : 'none';
              span.style.color = checked ? '#6c757d' : '';
            }
          });
        }
      });
      
      console.log('✅ 事件监听已设置');
    });
    
    // 切换折叠状态
    async function toggleFold(headingId) {
      const wrapper = document.querySelector(\`[data-heading-id="\${headingId}"]\`);
      const btn = wrapper.querySelector('.fold-btn');
      const heading = wrapper.querySelector('h1, h2, h3, h4, h5, h6');
      const isCollapsed = heading.classList.contains('collapsed');
      
      // 切换状态
      heading.classList.toggle('collapsed');
      btn.textContent = isCollapsed ? '▼' : '▶';
      
      // 折叠/展开后续内容
      let nextElement = wrapper.nextElementSibling;
      while (nextElement && !nextElement.classList.contains('heading-wrapper')) {
        nextElement.style.display = isCollapsed ? '' : 'none';
        nextElement = nextElement.nextElementSibling;
      }
      
      // 保存状态
      try {
        await fetch('/api/fold/toggle', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ 
            filePath: '${filePath}', 
            headingId, 
            collapsed: !isCollapsed 
          })
        });
      } catch (error) {
        console.error('更新折叠状态失败:', error);
      }
    }
    
    // 加载大纲
    async function loadOutline() {
      try {
        const response = await fetch(\`/api/outline?filePath=\${encodeURIComponent('${filePath}')}\`);
        const data = await response.json();
        
        if (data.success) {
          const outlineHtml = data.outline.map(h => 
            \`<div class="outline-item level-\${h.level}" onclick="scrollToHeading('\${h.id}')">\${h.text}</div>\`
          ).join('');
          
          document.getElementById('outline-content').innerHTML = outlineHtml;
        }
      } catch (error) {
        console.error('加载大纲失败:', error);
        document.getElementById('outline-content').innerHTML = '加载失败';
      }
    }
    
    // 滚动到标题
    function scrollToHeading(id) {
      const element = document.getElementById(id);
      if (element) {
        element.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }
    }
    
    // 页面加载完成后加载大纲
    loadOutline();
  </script>
</body>
</html>
  `);
});

// 文件列表页面(保持原功能)
app.get('/', (req, res) => {
  const dirPath = req.query.path || '';
  const fullPath = path.join(DIRECTORY, dirPath);
  
  if (!fs.existsSync(fullPath)) {
    return res.status(404).send('目录未找到');
  }
  
  const stats = fs.statSync(fullPath);
  
  if (stats.isDirectory()) {
    const files = fs.readdirSync(fullPath).map(f => {
      const fstat = fs.statSync(path.join(fullPath, f));
      return {
        name: f,
        isDirectory: fstat.isDirectory(),
        size: fstat.size,
        modified: fstat.mtime
      };
    });
    
    res.send(`
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>文件服务器</title>
</head>
<body>
  <h1>📁 ${dirPath || '根目录'}</h1>
  <ul>
    ${files.map(f => `
      <li>
        ${f.isDirectory ? '📁' : '📄'}
        <a href="${f.isDirectory ? `/?path=${path.join(dirPath, f.name)}` : `/view/${path.join(dirPath, f.name)}`}">
          ${f.name}
        </a>
        ${!f.isDirectory ? `<small>(${(f.size / 1024).toFixed(2)} KB)</small>` : ''}
      </li>
    `).join('')}
  </ul>
</body>
</html>
    `);
  } else {
    res.redirect(`/view/${dirPath}`);
  }
});

// 启动服务器
app.listen(PORT, () => {
  console.log(`🚀 动态文件服务器运行在端口 ${PORT}`);
  console.log(`📚 访问地址: http://localhost:${PORT}`);
  console.log(`💾 数据库: /data/ai-output/file_server.db`);
});

// API: 获取待办状态
app.get('/api/get-todo-state', (req, res) => {
  const { filePath } = req.query;
  
  try {
    const states = db.prepare(`
      SELECT item_index, checked FROM todo_items WHERE file_path = ?
    `).all(filePath);
    
    res.json({ success: true, states });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});