文件预览

docx.js

查看 Feishu Doc 技能包中的文件内容。

文件内容

lib/docx.js


async function fetchDocxContent(documentId, accessToken) {
  // 1. Get document info for title
  const infoUrl = `https://open.feishu.cn/open-apis/docx/v1/documents/${documentId}`;
  const infoRes = await fetch(infoUrl, {
    headers: { 'Authorization': `Bearer ${accessToken}` }
  });
  const infoData = await infoRes.json();
  let title = "Untitled Docx";
  if (infoData.code === 0 && infoData.data && infoData.data.document) {
    title = infoData.data.document.title;
  }

  // 2. Fetch all blocks
  // List blocks API: GET https://open.feishu.cn/open-apis/docx/v1/documents/{document_id}/blocks
  // Use pagination if necessary, fetching all for now (basic implementation)
  let blocks = [];
  let pageToken = '';
  let hasMore = true;

  while (hasMore) {
    const url = `https://open.feishu.cn/open-apis/docx/v1/documents/${documentId}/blocks?page_size=500${pageToken ? `&page_token=${pageToken}` : ''}`;
    const response = await fetch(url, {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });
    const data = await response.json();
    
    if (data.code !== 0) {
      throw new Error(`Failed to fetch docx blocks: ${data.msg}`);
    }

    if (data.data && data.data.items) {
      blocks = blocks.concat(data.data.items);
    }
    
    hasMore = data.data.has_more;
    pageToken = data.data.page_token;
  }

  const markdown = convertBlocksToMarkdown(blocks);
  return { title, content: markdown };
}

function convertBlocksToMarkdown(blocks) {
  if (!blocks || blocks.length === 0) return "";
  
  let md = [];
  
  for (const block of blocks) {
    const type = block.block_type;
    
    switch (type) {
      case 1: // page
        break;
      case 2: // text (paragraph)
        md.push(parseText(block.text));
        break;
      case 3: // heading1
        md.push(`# ${parseText(block.heading1)}`);
        break;
      case 4: // heading2
        md.push(`## ${parseText(block.heading2)}`);
        break;
      case 5: // heading3
        md.push(`### ${parseText(block.heading3)}`);
        break;
      case 6: // heading4
        md.push(`#### ${parseText(block.heading4)}`);
        break;
      case 7: // heading5
        md.push(`##### ${parseText(block.heading5)}`);
        break;
      case 8: // heading6
        md.push(`###### ${parseText(block.heading6)}`);
        break;
      case 9: // heading7
        md.push(`####### ${parseText(block.heading7)}`);
        break;
      case 10: // heading8
        md.push(`######## ${parseText(block.heading8)}`);
        break;
      case 11: // heading9
        md.push(`######### ${parseText(block.heading9)}`);
        break;
      case 12: // bullet
        md.push(`- ${parseText(block.bullet)}`);
        break;
      case 13: // ordered
        md.push(`1. ${parseText(block.ordered)}`);
        break;
      case 14: // code
        md.push('```' + (block.code?.style?.language === 1 ? '' : '') + '\n' + parseText(block.code) + '\n```');
        break;
      case 15: // quote
        md.push(`> ${parseText(block.quote)}`);
        break;
      case 27: // image
        md.push(`![Image](token:${block.image?.token})`);
        break;
      default:
        // Ignore unknown blocks for now
        console.error(`Skipped block type: ${type}`, JSON.stringify(block).substring(0, 200));
        md.push(`[UNSUPPORTED BLOCK TYPE: ${type}]`);
        break;
    }
  }
  
  return md.join('\n\n');
}

async function appendDocxContent(documentId, content, accessToken) {
  // 1. Convert markdown content to Feishu blocks
  const blocks = convertMarkdownToBlocks(content);
  
  // 2. Append to the end of the document (root block)
  // POST https://open.feishu.cn/open-apis/docx/v1/documents/{document_id}/blocks/{block_id}/children
  // Use documentId as block_id to append to root
  const url = `https://open.feishu.cn/open-apis/docx/v1/documents/${documentId}/blocks/${documentId}/children`;
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json; charset=utf-8'
    },
    body: JSON.stringify({
      children: blocks,
      index: -1 // Append to end
    })
  });

  const data = await response.json();
  if (data.code !== 0) {
    throw new Error(`Failed to append to docx: ${data.msg}`);
  }
  
  return { success: true, appended_blocks: data.data.children };
}

function convertMarkdownToBlocks(markdown) {
  // Simple parser: split by newlines, treat # as headers, others as text
  // For robustness, this should be a real parser. Here we implement a basic one.
  const lines = markdown.split('\n');
  const blocks = [];
  
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    
    if (trimmed.startsWith('# ')) {
      blocks.push({ block_type: 3, heading1: { elements: [{ text_run: { content: trimmed.substring(2) } }] } });
    } else if (trimmed.startsWith('## ')) {
      blocks.push({ block_type: 4, heading2: { elements: [{ text_run: { content: trimmed.substring(3) } }] } });
    } else if (trimmed.startsWith('### ')) {
      blocks.push({ block_type: 5, heading3: { elements: [{ text_run: { content: trimmed.substring(4) } }] } });
    } else if (trimmed.startsWith('- ')) {
      blocks.push({ block_type: 12, bullet: { elements: [{ text_run: { content: trimmed.substring(2) } }] } });
    } else {
      // Default to text (paragraph)
      blocks.push({ block_type: 2, text: { elements: [{ text_run: { content: line } }] } });
    }
  }
  return blocks;
}

function parseText(blockData) {
  if (!blockData || !blockData.elements) return "";
  
  return blockData.elements.map(el => {
    if (el.text_run) {
      let text = el.text_run.content;
      const style = el.text_run.text_element_style;
      if (style) {
        if (style.bold) text = `**${text}**`;
        if (style.italic) text = `*${text}*`;
        if (style.strikethrough) text = `~~${text}~~`;
        if (style.inline_code) text = `\`${text}\``;
        if (style.link) text = `[${text}](${style.link.url})`;
      }
      return text;
    }
    if (el.mention_doc) {
      return `[Doc: ${el.mention_doc.token}]`;
    }
    return "";
  }).join("");
}

module.exports = {
  fetchDocxContent,
  appendDocxContent
};