UNPKG

feishu-mcp

Version:

Model Context Protocol server for Feishu integration

499 lines (498 loc) 21.3 kB
import axios, { AxiosError } from "axios"; import { Logger } from "../server.js"; export class FeishuService { constructor(appId, appSecret) { Object.defineProperty(this, "appId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "appSecret", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "baseUrl", { enumerable: true, configurable: true, writable: true, value: "https://open.feishu.cn/open-apis" }); Object.defineProperty(this, "accessToken", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "tokenExpireTime", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "MAX_TOKEN_LIFETIME", { enumerable: true, configurable: true, writable: true, value: 2 * 60 * 60 * 1000 }); // 2小时的毫秒数 this.appId = appId; this.appSecret = appSecret; } isTokenExpired() { if (!this.accessToken || !this.tokenExpireTime) return true; return Date.now() >= this.tokenExpireTime; } async getAccessToken() { if (this.accessToken && !this.isTokenExpired()) { Logger.log('使用现有访问令牌,未过期'); return this.accessToken; } try { const url = `${this.baseUrl}/auth/v3/tenant_access_token/internal`; const requestData = { app_id: this.appId, app_secret: this.appSecret, }; Logger.log('开始获取新的访问令牌...'); Logger.log(`请求URL: ${url}`); Logger.log(`请求方法: POST`); Logger.log(`请求数据: ${JSON.stringify(requestData, null, 2)}`); const response = await axios.post(url, requestData); Logger.log(`响应状态码: ${response.status}`); Logger.log(`响应头: ${JSON.stringify(response.headers, null, 2)}`); Logger.log(`响应数据: ${JSON.stringify(response.data, null, 2)}`); if (response.data.code !== 0) { Logger.error(`获取访问令牌失败,错误码: ${response.data.code}, 错误信息: ${response.data.msg}`); throw { status: response.status, err: response.data.msg || "Unknown error", apiError: response.data }; } this.accessToken = response.data.tenant_access_token; this.tokenExpireTime = Date.now() + Math.min(response.data.expire * 1000, this.MAX_TOKEN_LIFETIME); Logger.log(`成功获取新的访问令牌,有效期: ${response.data.expire} 秒`); return this.accessToken; // 使用类型断言确保返回类型为string } catch (error) { if (error instanceof AxiosError && error.response) { Logger.error(`获取访问令牌请求失败:`); Logger.error(`状态码: ${error.response.status}`); Logger.error(`响应头: ${JSON.stringify(error.response.headers, null, 2)}`); Logger.error(`响应数据: ${JSON.stringify(error.response.data, null, 2)}`); throw { status: error.response.status, err: error.response.data?.msg || "Unknown error", apiError: error.response.data }; } Logger.error('获取访问令牌时发生未知错误:', error); throw new Error("Failed to get Feishu access token"); } } async request(endpoint, method = "GET", data) { try { const accessToken = await this.getAccessToken(); const url = `${this.baseUrl}${endpoint}`; const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }; Logger.log('准备发送请求:'); Logger.log(`请求URL: ${url}`); Logger.log(`请求方法: ${method}`); Logger.log(`请求头: ${JSON.stringify(headers, null, 2)}`); if (data) { Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`); } const response = await axios({ method, url, headers, data, }); Logger.log('收到响应:'); Logger.log(`响应状态码: ${response.status}`); Logger.log(`响应头: ${JSON.stringify(response.headers, null, 2)}`); Logger.log(`响应数据: ${JSON.stringify(response.data, null, 2)}`); return response.data; } catch (error) { if (error instanceof AxiosError && error.response) { Logger.error(`请求失败:`); Logger.error(`状态码: ${error.response.status}`); Logger.error(`响应头: ${JSON.stringify(error.response.headers, null, 2)}`); Logger.error(`响应数据: ${JSON.stringify(error.response.data, null, 2)}`); throw { status: error.response.status, err: error.response.data?.msg || "Unknown error", apiError: error.response.data }; } Logger.error('发送请求时发生未知错误:', error); throw new Error("Failed to make request to Feishu API"); } } // 创建新文档 async createDocument(title, folderToken) { try { Logger.log(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ',根目录'}`); const endpoint = '/docx/v1/documents'; const data = { title: title, }; if (folderToken) { data.folder_token = folderToken; } Logger.log(`准备请求API端点: ${endpoint}`); Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`); const response = await this.request(endpoint, 'POST', data); if (response.code !== 0) { throw new Error(`创建文档失败: ${response.msg}`); } const docInfo = response.data?.document; Logger.log(`文档创建成功,文档ID: ${docInfo?.document_id}`); Logger.log(`文档详情: ${JSON.stringify(docInfo, null, 2)}`); return docInfo; } catch (error) { Logger.error(`创建文档失败:`, error); if (error instanceof AxiosError) { Logger.error(`请求URL: ${error.config?.url}`); Logger.error(`请求方法: ${error.config?.method?.toUpperCase()}`); Logger.error(`状态码: ${error.response?.status}`); if (error.response?.data) { Logger.error(`错误详情: ${JSON.stringify(error.response.data, null, 2)}`); } } throw error; } } // 获取文档信息 async getDocumentInfo(documentId) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始获取文档信息,文档ID: ${docId}`); const endpoint = `/docx/v1/documents/${docId}`; Logger.log(`准备请求API端点: ${endpoint}`); const response = await this.request(endpoint); if (response.code !== 0) { throw new Error(`获取文档信息失败: ${response.msg}`); } const docInfo = response.data?.document; Logger.log(`文档信息获取成功: ${JSON.stringify(docInfo, null, 2)}`); if (!docInfo) { throw new Error(`获取文档信息失败: 返回的文档信息为空`); } return docInfo; } catch (error) { Logger.error(`获取文档信息失败:`, error); throw error; } } // 获取文档纯文本内容 async getDocumentContent(documentId, lang = 0) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始获取文档内容,文档ID: ${docId},语言: ${lang}`); const endpoint = `/docx/v1/documents/${docId}/raw_content?lang=${lang}`; Logger.log(`准备请求API端点: ${endpoint}`); const response = await this.request(endpoint); if (response.code !== 0) { throw new Error(`获取文档内容失败: ${response.msg}`); } Logger.log(`文档内容获取成功,长度: ${response.data?.content?.length || 0}字符`); return response.data?.content || ''; } catch (error) { Logger.error(`获取文档内容失败:`, error); throw error; } } // 获取文档块 async getDocumentBlocks(documentId, pageSize = 500) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始获取文档块,文档ID: ${docId},页大小: ${pageSize}`); const endpoint = `/docx/v1/documents/${docId}/blocks?document_revision_id=-1&page_size=${pageSize}`; Logger.log(`准备请求API端点: ${endpoint}`); const response = await this.request(endpoint); if (response.code !== 0) { throw new Error(`获取文档块失败: ${response.msg}`); } const blocks = response.data?.items || []; Logger.log(`文档块获取成功,共 ${blocks.length} 个块`); return blocks; } catch (error) { Logger.error(`获取文档块失败:`, error); throw error; } } // 创建代码块 async createCodeBlock(documentId, parentBlockId, code, language = 0, wrap = false, index = 0) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始创建代码块,文档ID: ${docId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`); const blockContent = { block_type: 14, // 14表示代码块 code: { elements: [ { text_run: { content: code, text_element_style: { bold: false, inline_code: false, italic: false, strikethrough: false, underline: false } } } ], style: { language: language, wrap: wrap } } }; Logger.log(`代码块内容: ${JSON.stringify(blockContent, null, 2)}`); return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index); } catch (error) { Logger.error(`创建代码块失败:`, error); throw error; } } // 创建文本块 async createTextBlock(documentId, parentBlockId, textContents, align = 1, index = 0) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始创建文本块,文档ID: ${docId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`); const blockContent = { block_type: 2, // 2表示文本块 text: { elements: textContents.map(content => ({ text_run: { content: content.text, text_element_style: content.style || {} } })), style: { align: align // 1 居左,2 居中,3 居右 } } }; Logger.log(`文本块内容: ${JSON.stringify(blockContent, null, 2)}`); return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index); } catch (error) { Logger.error(`创建文本块失败:`, error); throw error; } } // 创建文档块 async createDocumentBlock(documentId, parentBlockId, blockContent, index = 0) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始创建文档块,文档ID: ${docId},父块ID: ${parentBlockId},插入位置: ${index}`); const endpoint = `/docx/v1/documents/${docId}/blocks/${parentBlockId}/children?document_revision_id=-1`; Logger.log(`准备请求API端点: ${endpoint}`); const data = { children: [blockContent], index: index }; Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`); const response = await this.request(endpoint, 'POST', data); if (response.code !== 0) { throw new Error(`创建文档块失败: ${response.msg}`); } Logger.log(`文档块创建成功: ${JSON.stringify(response.data, null, 2)}`); return response.data; } catch (error) { Logger.error(`创建文档块失败:`, error); throw error; } } // 创建文本块内容 createTextBlockContent(textContents, align = 1) { return { block_type: 2, // 2表示文本块 text: { elements: textContents.map(content => ({ text_run: { content: content.text, text_element_style: content.style || {} } })), style: { align: align // 1 居左,2 居中,3 居右 } } }; } // 创建代码块内容 createCodeBlockContent(code, language = 0, wrap = false) { return { block_type: 14, // 14表示代码块 code: { elements: [ { text_run: { content: code, text_element_style: { bold: false, inline_code: false, italic: false, strikethrough: false, underline: false } } } ], style: { language: language, wrap: wrap } } }; } // 创建标题块内容 createHeadingBlockContent(text, level = 1, align = 1) { // 确保标题级别在有效范围内(1-9) const safeLevel = Math.max(1, Math.min(9, level)); // 根据标题级别设置block_type和对应的属性名 // 飞书API中,一级标题的block_type为3,二级标题为4,以此类推 const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推 const headingKey = `heading${safeLevel}`; // heading1, heading2, ... // 构建块内容 const blockContent = { block_type: blockType }; // 设置对应级别的标题属性 blockContent[headingKey] = { elements: [ { text_run: { content: text, text_element_style: {} } } ], style: { align: align, folded: false } }; return blockContent; } // 批量创建文档块 async createDocumentBlocks(documentId, parentBlockId, blockContents, index = 0) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始批量创建文档块,文档ID: ${docId},父块ID: ${parentBlockId},块数量: ${blockContents.length},插入位置: ${index}`); const endpoint = `/docx/v1/documents/${docId}/blocks/${parentBlockId}/children?document_revision_id=-1`; Logger.log(`准备请求API端点: ${endpoint}`); const data = { children: blockContents, index: index }; Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`); const response = await this.request(endpoint, 'POST', data); if (response.code !== 0) { throw new Error(`批量创建文档块失败: ${response.msg}`); } Logger.log(`批量文档块创建成功: ${JSON.stringify(response.data, null, 2)}`); return response.data; } catch (error) { Logger.error(`批量创建文档块失败:`, error); throw error; } } // 创建标题块 async createHeadingBlock(documentId, parentBlockId, text, level = 1, index = 0, align = 1) { try { const docId = this.extractDocIdFromUrl(documentId); if (!docId) { throw new Error(`无效的文档ID: ${documentId}`); } Logger.log(`开始创建标题块,文档ID: ${docId},父块ID: ${parentBlockId},标题级别: ${level},插入位置: ${index}`); // 确保标题级别在有效范围内(1-9) const safeLevel = Math.max(1, Math.min(9, level)); // 根据标题级别设置block_type和对应的属性名 // 飞书API中,一级标题的block_type为3,二级标题为4,以此类推 const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推 const headingKey = `heading${safeLevel}`; // heading1, heading2, ... // 构建块内容 const blockContent = { block_type: blockType }; // 设置对应级别的标题属性 blockContent[headingKey] = { elements: [ { text_run: { content: text, text_element_style: {} } } ], style: { align: align, folded: false } }; Logger.log(`标题块内容: ${JSON.stringify(blockContent, null, 2)}`); return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index); } catch (error) { Logger.error(`创建标题块失败:`, error); throw error; } } extractDocIdFromUrl(url) { // 处理飞书文档 URL,提取文档 ID // 支持多种URL格式 // 1. 标准文档URL格式: https://xxx.feishu.cn/docs/xxx 或 https://xxx.feishu.cn/docx/xxx const docxMatch = url.match(/\/docx\/(\w+)/); // 匹配 docx 格式 const docsMatch = url.match(/\/docs\/(\w+)/); // 匹配 docs 格式 // 2. API URL格式: https://open.feishu.cn/open-apis/doc/v2/documents/xxx const apiMatch = url.match(/\/documents\/([\w-]+)/); // 匹配 API URL 格式 // 3. 直接使用文档ID const directIdMatch = url.match(/^([\w-]+)$/); // 如果直接传入了文档ID // 按优先级返回匹配结果 return docxMatch ? docxMatch[1] : docsMatch ? docsMatch[1] : apiMatch ? apiMatch[1] : directIdMatch ? directIdMatch[1] : null; } }