    renderMarkdown(text) {
      if (!text) return ''
      // 1. 转义 HTML
      var h = escapeHtml(text)
      // 2. 提取保护块：代码块 → 占位符
      var codeBlocks = []
      h = h.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
        var idx = codeBlocks.length
        codeBlocks.push({ lang: lang, code: code })
        return '___CB' + idx + '___'
      })
      // 3. 按双换行切分为段落块
      var blocks = h.split(/\n\n+/)
      var result = ''
      var inUl = false, inOl = false
      for (var bi = 0; bi < blocks.length; bi++) {
        var block = blocks[bi].trim()
        if (!block) continue
        var blockLines = block.split('\n')
        var allUlItems = true, allOlItems = true
        for (var li = 0; li < blockLines.length; li++) {
          var bline = blockLines[li].trim()
          if (!bline) continue
          if (!/^[\-\*]\s/.test(bline)) allUlItems = false
          if (!/^\d+\.\s/.test(bline)) allOlItems = false
        }
        if (allUlItems && blockLines.some(function(l) { return /^[\-\*]\s/.test(l.trim()) })) {
          if (!inUl) { result += '<ul>'; inUl = true }
          for (var ui = 0; ui < blockLines.length; ui++) {
            var uline = blockLines[ui].trim()
            if (!uline) continue
            result += '<li>' + this._renderInline(uline.replace(/^[\-\*]\s+/, '')) + '</li>'
          }
          continue
        } else if (inUl) { result += '</ul>'; inUl = false }
        if (allOlItems && blockLines.some(function(l) { return /^\d+\.\s/.test(l.trim()) })) {
          if (!inOl) { result += '<ol>'; inOl = true }
          for (var oi = 0; oi < blockLines.length; oi++) {
            var oline = blockLines[oi].trim()
            if (!oline) continue
            result += '<li>' + this._renderInline(oline.replace(/^\d+\.\s+/, '')) + '</li>'
          }
          continue
        } else if (inOl) { result += '</ol>'; inOl = false }
        if (inUl) { result += '</ul>'; inUl = false }
        if (inOl) { result += '</ol>'; inOl = false }
        if (/^###\s/.test(block)) {
          result += '<h3>' + this._renderInline(block.replace(/^###\s+/, '')) + '</h3>'
        } else if (/^##\s/.test(block)) {
          result += '<h2>' + this._renderInline(block.replace(/^##\s+/, '')) + '</h2>'
        } else if (/^#\s/.test(block)) {
          result += '<h1>' + this._renderInline(block.replace(/^#\s+/, '')) + '</h1>'
        } else if (/^>\s/.test(block)) {
          result += '<blockquote><p>' + this._renderInline(block.replace(/^>\s?/, '').replace(/\n/g, '<br>')) + '</p></blockquote>'
        } else if (/^---/.test(block) || /^\*\*\*/.test(block)) {
          result += '<hr>'
        } else if (/\|.*\|/.test(block)) {
          result += this._renderTable(block)
        } else {
          result += '<p>' + this._renderInline(block.replace(/\n/g, '<br>')) + '</p>'
        }
      }
      if (inUl) result += '</ul>'
      if (inOl) result += '</ol>'
      // 4. 还原代码块
      for (var ci = 0; ci < codeBlocks.length; ci++) {
        var cb = codeBlocks[ci]
        var codeHtml = '<div class="xzd-code-block-wrapper"><pre><code>' + cb.code + '</code></pre><button class="xzd-code-copy-btn" data-code="' + escapeHtml(cb.code).replace(/"/g, '&quot;') + '">复制代码</button></div>'
        result = result.replace('___CB' + ci + '___', codeHtml)
      }
      return result
    }
    _renderInline(text) {
      if (!text) return ''
      var codes = []
      text = text.replace(/`([^`]+)`/g, function (_, code) {
        var idx = codes.length
        codes.push('<code>' + code + '</code>')
        return '___IC' + idx + '___'
      })
      text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
      text = text.replace(/\*(.+?)\*/g, '<em>$1</em>')
      text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
      for (var ci = 0; ci < codes.length; ci++) {
        text = text.replace('___IC' + ci + '___', codes[ci])
      }
      return text
    }
    _renderTable(block) {
      var tableLines = block.split('\n').filter(function(l) { return l.trim() })
      if (tableLines.length < 2) return '<p>' + this._renderInline(block) + '</p>'
      var hasHeader = tableLines.length >= 2 && /^\|[\s\-:|]+\|$/.test(tableLines[1])
      var html = '<table>'
      var startRow = 0
      if (hasHeader) {
        html += '<thead><tr>'
        var headers = tableLines[0].split('|').filter(function(c) { return c.trim() })
        for (var hi = 0; hi < headers.length; hi++) {
          html += '<th>' + this._renderInline(headers[hi].trim()) + '</th>'
        }
        html += '</tr></thead><tbody>'
        startRow = 2
      } else {
        html += '<tbody>'
      }
      for (var ri = startRow; ri < tableLines.length; ri++) {
        var cells = tableLines[ri].split('|').filter(function(c) { return c.trim() })
        if (cells.length === 0) continue
        html += '<tr>'
        for (var ci = 0; ci < cells.length; ci++) {
          html += '<td>' + this._renderInline(cells[ci].trim()) + '</td>'
        }
        html += '</tr>'
      }
      html += '</tbody></table>'
      return html
    }
