feishu-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/cli.js +15 -0
- package/dist/config.js +104 -0
- package/dist/index.js +32 -0
- package/dist/server.js +314 -0
- package/dist/services/feishu.js +396 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 cso1z
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# 飞书 MCP 服务器
|
|
2
|
+
|
|
3
|
+
为 [Cursor](https://cursor.sh/)、[Windsurf](https://codeium.com/windsurf)、[Cline](https://cline.bot/) 和其他 AI 驱动的编码工具提供访问飞书文档的能力,基于 [Model Context Protocol](https://modelcontextprotocol.io/introduction) 服务器实现。
|
|
4
|
+
|
|
5
|
+
当 Cursor 能够访问飞书文档数据时,它可以更准确地理解和处理文档内容,比其他方法(如复制粘贴文本)更加高效。
|
|
6
|
+
|
|
7
|
+
## 核心功能
|
|
8
|
+
|
|
9
|
+
### 文档管理
|
|
10
|
+
- **创建飞书文档**:支持创建新的飞书文档
|
|
11
|
+
|
|
12
|
+
### 文档内容操作
|
|
13
|
+
- **获取文档信息**:能够获取文档中各个块的详细信息
|
|
14
|
+
- **获取文档纯文本内容**:支持提取文档的纯文本内容
|
|
15
|
+
- **修改文档内容**:
|
|
16
|
+
- 更新现有文档的内容
|
|
17
|
+
- 插入新的内容块
|
|
18
|
+
|
|
19
|
+
### 计划中的功能
|
|
20
|
+
- **高级内容插入**:
|
|
21
|
+
- 插入图表:支持各类数据可视化图表
|
|
22
|
+
- 插入流程图:支持流程图和思维导图
|
|
23
|
+
- 插入公式:支持数学公式和科学符号
|
|
24
|
+
- 图表、流程图的内容识别
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## 工作原理
|
|
28
|
+
|
|
29
|
+
1. 在 Cursor 的 Agent 模式下打开编辑器。
|
|
30
|
+
2. 粘贴飞书文档的链接。
|
|
31
|
+
3. 要求 Cursor 基于飞书文档执行操作——例如,分析文档内容或创建相关代码。
|
|
32
|
+
4. Cursor 将从飞书获取相关元数据并使用它来辅助编写代码。
|
|
33
|
+
|
|
34
|
+
这个 MCP 服务器专为 Cursor 设计。在响应来自[飞书 API](https://open.feishu.cn/document/home/introduction-to-lark-open-platform/overview) 的内容之前,它会简化和转换响应,确保只向模型提供最相关的文档信息。
|
|
35
|
+
|
|
36
|
+
### 从本地源代码运行服务器
|
|
37
|
+
|
|
38
|
+
1. 克隆仓库
|
|
39
|
+
2. 使用 `pnpm install` 安装依赖
|
|
40
|
+
3. 复制 `.env.example` 到 `.env` 并填入你的[飞书应用凭证](https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes/create-an-app)。
|
|
41
|
+
4. 使用 `pnpm run dev` 运行服务器,可以使用[命令行参数](#命令行参数)部分的任何标志。
|
|
42
|
+
|
|
43
|
+
## 配置
|
|
44
|
+
|
|
45
|
+
服务器可以使用环境变量(通过 `.env` 文件)或命令行参数进行配置。命令行参数优先于环境变量。
|
|
46
|
+
|
|
47
|
+
### 环境变量
|
|
48
|
+
|
|
49
|
+
- `FEISHU_APP_ID`:你的[飞书应用 ID](https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes/create-an-app)(必需)
|
|
50
|
+
- `FEISHU_APP_SECRET`:你的[飞书应用密钥](https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes/create-an-app)(必需)
|
|
51
|
+
- `PORT`:运行服务器的端口(默认:3333)
|
|
52
|
+
|
|
53
|
+
### 命令行参数
|
|
54
|
+
|
|
55
|
+
- `--version`:显示版本号
|
|
56
|
+
- `--feishu-app-id`:你的飞书应用 ID
|
|
57
|
+
- `--feishu-app-secret`:你的飞书应用密钥
|
|
58
|
+
- `--port`:运行服务器的端口
|
|
59
|
+
- `--stdio`:在命令模式下运行服务器,而不是默认的 HTTP/SSE
|
|
60
|
+
- `--help`:显示帮助菜单
|
|
61
|
+
|
|
62
|
+
## 连接到 Cursor
|
|
63
|
+
|
|
64
|
+
### 配置 Cursor
|
|
65
|
+
|
|
66
|
+
1. 打开 Cursor 设置
|
|
67
|
+
2. 导航到 `Settings > AI > MCP Servers`
|
|
68
|
+
3. 添加新服务器,URL 为 `http://localhost:3333`(或你配置的端口)
|
|
69
|
+
4. 点击 "Verify Connection" 确保连接成功
|
|
70
|
+
|
|
71
|
+
## 使用方法
|
|
72
|
+
|
|
73
|
+
1. 在 Cursor 中,打开 AI 面板(默认快捷键 `Cmd+K` 或 `Ctrl+K`)
|
|
74
|
+
2. 如果需要新建一个飞书文档编辑信息,应该明确制定一个folderToken,可以打开一个飞书文档目录如:`https://vq5xxxxx7bc.feishu.cn/drive/folder/FPKvfjdxxxxx706RnOc查找`
|
|
75
|
+
2. 如果需要修改飞书文档内容应该明确告知飞书文档链接,例如:`https://vq5ixxxx7bc.feishu.cn/docx/J6T0d6exxxxxxxDdc1zqwnph`
|
|
76
|
+
3. 询问关于文档的问题或请求基于文档内容执行操作
|
|
77
|
+
4. 创建编辑文档都需要权限,可以到飞书开放平台对账号进行测试`https://open.feishu.cn/api-explorer/cli_a75a8ca0ac79100c?apiName=tenant_access_token_internal&from=op_doc&project=auth&resource=auth&version=v3`
|
|
78
|
+
|
|
79
|
+
## Cursor最佳实践
|
|
80
|
+
|
|
81
|
+
添加Rules指导模型操作流程
|
|
82
|
+
|
|
83
|
+
`在将文档上传至飞书时,请遵循以下操作指南:1. 若未特别指定 folderToken,默认为 FPKvf*********6RnOc。2. 在块创建失败的情况下,通过查询文档中所有的块信息,以确认是否确实发生了失败。3. 若需在现有文档中追加信息,请先获取该文档的所有块信息,并根据返回结果确定要插入的内容及其索引位置。4. 一旦文档内容全部修改完成,请提供文档链接,格式如下: https://vq5iay***bc.feishu.cn/docx/documentId。5.获取文档信息时应优先查询其纯文本内容,如果不满足则通过查询所有块来确定内容`
|
|
84
|
+
## 许可证
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { config } from "dotenv";
|
|
4
|
+
import { startServer } from "./index";
|
|
5
|
+
// Load .env from the current working directory
|
|
6
|
+
config({ path: resolve(process.cwd(), ".env") });
|
|
7
|
+
startServer().catch((error) => {
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
console.error("Failed to start server:", error.message);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
console.error("Failed to start server with unknown error:", error);
|
|
13
|
+
}
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { config } from "dotenv";
|
|
2
|
+
import yargs from "yargs";
|
|
3
|
+
import { hideBin } from "yargs/helpers";
|
|
4
|
+
// 确保在任何配置读取前加载.env文件
|
|
5
|
+
config();
|
|
6
|
+
function maskApiKey(key) {
|
|
7
|
+
if (key.length <= 4)
|
|
8
|
+
return "****";
|
|
9
|
+
return `****${key.slice(-4)}`;
|
|
10
|
+
}
|
|
11
|
+
export function getServerConfig(isStdioMode) {
|
|
12
|
+
// Parse command line arguments
|
|
13
|
+
const argv = yargs(hideBin(process.argv))
|
|
14
|
+
.options({
|
|
15
|
+
port: {
|
|
16
|
+
type: "number",
|
|
17
|
+
description: "Port to run the server on",
|
|
18
|
+
},
|
|
19
|
+
"feishu-app-id": {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Feishu App ID",
|
|
22
|
+
},
|
|
23
|
+
"feishu-app-secret": {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Feishu App Secret",
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
.help()
|
|
29
|
+
.parseSync();
|
|
30
|
+
const config = {
|
|
31
|
+
port: 3333,
|
|
32
|
+
configSources: {
|
|
33
|
+
port: "default",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// Handle PORT
|
|
37
|
+
if (argv.port) {
|
|
38
|
+
config.port = argv.port;
|
|
39
|
+
config.configSources.port = "cli";
|
|
40
|
+
}
|
|
41
|
+
else if (process.env.PORT) {
|
|
42
|
+
config.port = parseInt(process.env.PORT, 10);
|
|
43
|
+
config.configSources.port = "env";
|
|
44
|
+
}
|
|
45
|
+
// 在加载环境变量之前添加日志
|
|
46
|
+
console.log('开始加载环境变量配置...');
|
|
47
|
+
console.log('当前环境变量 FEISHU_APP_ID:', process.env.FEISHU_APP_ID);
|
|
48
|
+
console.log('当前环境变量 FEISHU_APP_SECRET:', process.env.FEISHU_APP_SECRET);
|
|
49
|
+
// Handle Feishu configuration
|
|
50
|
+
if (argv["feishu-app-id"]) {
|
|
51
|
+
config.feishuAppId = argv["feishu-app-id"];
|
|
52
|
+
config.configSources.feishuAppId = "cli";
|
|
53
|
+
console.log(`飞书应用 ID 来自命令行参数: ${maskApiKey(config.feishuAppId)}`);
|
|
54
|
+
}
|
|
55
|
+
else if (process.env.FEISHU_APP_ID) {
|
|
56
|
+
config.feishuAppId = process.env.FEISHU_APP_ID;
|
|
57
|
+
config.configSources.feishuAppId = "env";
|
|
58
|
+
console.log(`飞书应用 ID 来自环境变量: ${maskApiKey(config.feishuAppId)}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log('未提供飞书应用 ID');
|
|
62
|
+
}
|
|
63
|
+
if (argv["feishu-app-secret"]) {
|
|
64
|
+
config.feishuAppSecret = argv["feishu-app-secret"];
|
|
65
|
+
config.configSources.feishuAppSecret = "cli";
|
|
66
|
+
console.log(`飞书应用密钥来自命令行参数: ${maskApiKey(config.feishuAppSecret)}`);
|
|
67
|
+
}
|
|
68
|
+
else if (process.env.FEISHU_APP_SECRET) {
|
|
69
|
+
config.feishuAppSecret = process.env.FEISHU_APP_SECRET;
|
|
70
|
+
config.configSources.feishuAppSecret = "env";
|
|
71
|
+
console.log(`飞书应用密钥来自环境变量: ${maskApiKey(config.feishuAppSecret)}`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log('未提供飞书应用密钥');
|
|
75
|
+
}
|
|
76
|
+
// 输出飞书配置状态总结
|
|
77
|
+
if (config.feishuAppId && config.feishuAppSecret) {
|
|
78
|
+
console.log('飞书配置已完整提供,服务将被初始化');
|
|
79
|
+
}
|
|
80
|
+
else if (config.feishuAppId || config.feishuAppSecret) {
|
|
81
|
+
console.log('飞书配置不完整,服务将不会初始化');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('未提供飞书配置,服务将不会初始化');
|
|
85
|
+
}
|
|
86
|
+
// 验证配置
|
|
87
|
+
if (!config.feishuAppId || !config.feishuAppSecret) {
|
|
88
|
+
console.error("FEISHU_APP_ID 和 FEISHU_APP_SECRET 是必需的(通过命令行参数 --feishu-app-id 和 --feishu-app-secret 或 .env 文件)");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
// Log configuration sources
|
|
92
|
+
if (!isStdioMode) {
|
|
93
|
+
console.log("\n配置信息:");
|
|
94
|
+
console.log(`- PORT: ${config.port} (来源: ${config.configSources.port})`);
|
|
95
|
+
if (config.feishuAppId) {
|
|
96
|
+
console.log(`- FEISHU_APP_ID: ${maskApiKey(config.feishuAppId)} (来源: ${config.configSources.feishuAppId})`);
|
|
97
|
+
}
|
|
98
|
+
if (config.feishuAppSecret) {
|
|
99
|
+
console.log(`- FEISHU_APP_SECRET: ${maskApiKey(config.feishuAppSecret)} (来源: ${config.configSources.feishuAppSecret})`);
|
|
100
|
+
}
|
|
101
|
+
console.log(); // 空行,提高可读性
|
|
102
|
+
}
|
|
103
|
+
return config;
|
|
104
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { FeishuMcpServer } from "./server";
|
|
3
|
+
import { getServerConfig } from "./config";
|
|
4
|
+
export async function startServer() {
|
|
5
|
+
// Check if we're running in stdio mode (e.g., via CLI)
|
|
6
|
+
const isStdioMode = process.env.NODE_ENV === "cli" || process.argv.includes("--stdio");
|
|
7
|
+
const config = getServerConfig(isStdioMode);
|
|
8
|
+
// 创建飞书配置对象
|
|
9
|
+
const feishuConfig = {
|
|
10
|
+
appId: config.feishuAppId,
|
|
11
|
+
appSecret: config.feishuAppSecret
|
|
12
|
+
};
|
|
13
|
+
console.log("Feishu configuration status: Available");
|
|
14
|
+
console.log(`Feishu App ID: ${feishuConfig.appId.substring(0, 4)}...${feishuConfig.appId.substring(feishuConfig.appId.length - 4)}`);
|
|
15
|
+
console.log(`Feishu App Secret: ${feishuConfig.appSecret.substring(0, 4)}...${feishuConfig.appSecret.substring(feishuConfig.appSecret.length - 4)}`);
|
|
16
|
+
const server = new FeishuMcpServer(feishuConfig);
|
|
17
|
+
if (isStdioMode) {
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(`Initializing Feishu MCP Server in HTTP mode on port ${config.port}...`);
|
|
23
|
+
await server.startHttpServer(config.port);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// If this file is being run directly, start the server
|
|
27
|
+
if (require.main === module) {
|
|
28
|
+
startServer().catch((error) => {
|
|
29
|
+
console.error("Failed to start server:", error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { FeishuService } from "./services/feishu";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
+
export const Logger = {
|
|
7
|
+
log: (...args) => { console.log(...args); },
|
|
8
|
+
error: (...args) => { console.error(...args); },
|
|
9
|
+
};
|
|
10
|
+
export class FeishuMcpServer {
|
|
11
|
+
constructor(feishuConfig) {
|
|
12
|
+
Object.defineProperty(this, "server", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "sseTransport", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: null
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "feishuService", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: null
|
|
29
|
+
});
|
|
30
|
+
// 详细记录飞书配置状态
|
|
31
|
+
Logger.log(`飞书配置已提供 - AppID: ${feishuConfig.appId.substring(0, 4)}...${feishuConfig.appId.substring(feishuConfig.appId.length - 4)}, AppSecret: ${feishuConfig.appSecret.substring(0, 4)}...${feishuConfig.appSecret.substring(feishuConfig.appSecret.length - 4)}`);
|
|
32
|
+
try {
|
|
33
|
+
this.feishuService = new FeishuService(feishuConfig.appId, feishuConfig.appSecret);
|
|
34
|
+
Logger.log('飞书服务初始化成功');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
Logger.error('飞书服务初始化失败:', error);
|
|
38
|
+
throw new Error('飞书服务初始化失败');
|
|
39
|
+
}
|
|
40
|
+
this.server = new McpServer({
|
|
41
|
+
name: "Feishu MCP Server",
|
|
42
|
+
version: "0.0.1",
|
|
43
|
+
}, {
|
|
44
|
+
capabilities: {
|
|
45
|
+
logging: {},
|
|
46
|
+
tools: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
this.registerTools();
|
|
50
|
+
}
|
|
51
|
+
registerTools() {
|
|
52
|
+
// 添加创建飞书文档工具
|
|
53
|
+
// 添加创建飞书文档工具
|
|
54
|
+
this.server.tool("create_feishu_doc", "Create a new Feishu document", {
|
|
55
|
+
title: z.string().describe("Document title"),
|
|
56
|
+
folderToken: z.string().optional().describe("Folder token where the document will be created. If not provided, the document will be created in the root directory"),
|
|
57
|
+
}, async ({ title, folderToken }) => {
|
|
58
|
+
try {
|
|
59
|
+
Logger.log(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ''}`);
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
const newDoc = await this.feishuService.createDocument(title, folderToken);
|
|
62
|
+
Logger.log(`飞书文档创建成功,文档ID: ${newDoc?.objToken || newDoc?.document_id}`);
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: JSON.stringify(newDoc, null, 2) }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
Logger.error(`创建飞书文档失败:`, error);
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: `创建飞书文档失败: ${error}` }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// 添加获取飞书文档信息工具
|
|
75
|
+
// this.server.tool(
|
|
76
|
+
// "get_feishu_doc_info",
|
|
77
|
+
// "Get basic information about a Feishu document",
|
|
78
|
+
// {
|
|
79
|
+
// documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
|
|
80
|
+
// },
|
|
81
|
+
// async ({ documentId }) => {
|
|
82
|
+
// try {
|
|
83
|
+
// if (!this.feishuService) {
|
|
84
|
+
// return {
|
|
85
|
+
// content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
86
|
+
// };
|
|
87
|
+
// }
|
|
88
|
+
// Logger.log(`开始获取飞书文档信息,文档ID: ${documentId}`);
|
|
89
|
+
// const docInfo = await this.feishuService.getDocumentInfo(documentId);
|
|
90
|
+
// Logger.log(`飞书文档信息获取成功,标题: ${docInfo.title}`);
|
|
91
|
+
// return {
|
|
92
|
+
// content: [{ type: "text", text: JSON.stringify(docInfo, null, 2) }],
|
|
93
|
+
// };
|
|
94
|
+
// } catch (error) {
|
|
95
|
+
// Logger.error(`获取飞书文档信息失败:`, error);
|
|
96
|
+
// return {
|
|
97
|
+
// content: [{ type: "text", text: `获取飞书文档信息失败: ${error}` }],
|
|
98
|
+
// };
|
|
99
|
+
// }
|
|
100
|
+
// },
|
|
101
|
+
// );
|
|
102
|
+
// 添加获取飞书文档内容工具
|
|
103
|
+
this.server.tool("get_feishu_doc_content", "Get the plain text content of a Feishu document", {
|
|
104
|
+
documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
|
|
105
|
+
lang: z.number().optional().default(0).describe("Language code. Default is 0 (Chinese)"),
|
|
106
|
+
}, async ({ documentId, lang }) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!this.feishuService) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
Logger.log(`开始获取飞书文档内容,文档ID: ${documentId},语言: ${lang}`);
|
|
114
|
+
const content = await this.feishuService.getDocumentContent(documentId, lang);
|
|
115
|
+
Logger.log(`飞书文档内容获取成功,内容长度: ${content.length}字符`);
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: content }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
Logger.error(`获取飞书文档内容失败:`, error);
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: `获取飞书文档内容失败: ${error}` }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// 添加获取飞书文档块工具
|
|
128
|
+
this.server.tool("get_feishu_doc_blocks", "When document structure is needed, obtain the block information about the Feishu document for content analysis or block insertion", {
|
|
129
|
+
documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
|
|
130
|
+
pageSize: z.number().optional().default(500).describe("Number of blocks per page. Default is 500"),
|
|
131
|
+
}, async ({ documentId, pageSize }) => {
|
|
132
|
+
try {
|
|
133
|
+
if (!this.feishuService) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
Logger.log(`开始获取飞书文档块,文档ID: ${documentId},页大小: ${pageSize}`);
|
|
139
|
+
const blocks = await this.feishuService.getDocumentBlocks(documentId, pageSize);
|
|
140
|
+
Logger.log(`飞书文档块获取成功,共 ${blocks.length} 个块`);
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: JSON.stringify(blocks, null, 2) }],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
Logger.error(`获取飞书文档块失败:`, error);
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: `获取飞书文档块失败: ${error}` }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// 添加创建飞书文档块工具
|
|
153
|
+
this.server.tool("create_feishu_text_block", "Create a new text block in a Feishu document (AI will automatically convert Markdown syntax to corresponding style attributes: **bold** → bold:true, *italic* → italic:true, ~~strikethrough~~ → strikethrough:true, `code` → inline_code:true)", {
|
|
154
|
+
documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
|
|
155
|
+
parentBlockId: z.string().describe("Parent block ID (NOT URL) where the new block will be added as a child. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter"),
|
|
156
|
+
textContents: z.array(z.object({
|
|
157
|
+
text: z.string().describe("Text content"),
|
|
158
|
+
style: z.object({
|
|
159
|
+
bold: z.boolean().optional().describe("Whether to make text bold. Default is false"),
|
|
160
|
+
italic: z.boolean().optional().describe("Whether to make text italic. Default is false"),
|
|
161
|
+
underline: z.boolean().optional().describe("Whether to add underline. Default is false"),
|
|
162
|
+
strikethrough: z.boolean().optional().describe("Whether to add strikethrough. Default is false"),
|
|
163
|
+
inline_code: z.boolean().optional().describe("Whether to format as inline code. Default is false"),
|
|
164
|
+
text_color: z.number().optional().describe("Text color as a number. Default is 0")
|
|
165
|
+
}).optional().describe("Text style settings")
|
|
166
|
+
})).describe("Array of text content objects. A block can contain multiple text segments with different styles"),
|
|
167
|
+
align: z.number().optional().default(1).describe("Text alignment: 1 for left, 2 for center, 3 for right. Default is 1"),
|
|
168
|
+
index: z.number().optional().default(0).describe("Insertion position index. Default is 0 (insert at the beginning). If unsure about the position, use the get_feishu_doc_blocks tool first to understand the document structure. For consecutive insertions, calculate the next position as previous_index + 1 to avoid querying document structure repeatedly")
|
|
169
|
+
}, async ({ documentId, parentBlockId, textContents, align = 1, index }) => {
|
|
170
|
+
try {
|
|
171
|
+
if (!this.feishuService) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// 处理Markdown语法转换
|
|
177
|
+
const processedTextContents = textContents.map(content => {
|
|
178
|
+
let { text, style = {} } = content;
|
|
179
|
+
// 创建一个新的style对象,避免修改原始对象
|
|
180
|
+
const newStyle = { ...style };
|
|
181
|
+
// 处理粗体 **text**
|
|
182
|
+
if (text.match(/\*\*([^*]+)\*\*/g)) {
|
|
183
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
|
|
184
|
+
newStyle.bold = true;
|
|
185
|
+
}
|
|
186
|
+
// 处理斜体 *text*
|
|
187
|
+
if (text.match(/(?<!\*)\*([^*]+)\*(?!\*)/g)) {
|
|
188
|
+
text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
|
|
189
|
+
newStyle.italic = true;
|
|
190
|
+
}
|
|
191
|
+
// 处理删除线 ~~text~~
|
|
192
|
+
if (text.match(/~~([^~]+)~~/g)) {
|
|
193
|
+
text = text.replace(/~~([^~]+)~~/g, "$1");
|
|
194
|
+
newStyle.strikethrough = true;
|
|
195
|
+
}
|
|
196
|
+
// 处理行内代码 `code`
|
|
197
|
+
if (text.match(/`([^`]+)`/g)) {
|
|
198
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
199
|
+
newStyle.inline_code = true;
|
|
200
|
+
}
|
|
201
|
+
return { text, style: newStyle };
|
|
202
|
+
});
|
|
203
|
+
Logger.log(`开始创建飞书文本块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
204
|
+
const result = await this.feishuService.createTextBlock(documentId, parentBlockId, processedTextContents, align, index);
|
|
205
|
+
Logger.log(`飞书文本块创建成功`);
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
Logger.error(`创建飞书文本块失败:`, error);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `创建飞书文本块失败: ${error}` }],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// 添加创建飞书代码块工具
|
|
218
|
+
this.server.tool("create_feishu_code_block", "Create a new code block in a Feishu document", {
|
|
219
|
+
documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
|
|
220
|
+
parentBlockId: z.string().describe("Parent block ID (NOT URL) where the new block will be added as a child. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter"),
|
|
221
|
+
code: z.string().describe("Code content"),
|
|
222
|
+
language: z.number().optional().default(0).describe("Programming language code as a number. Examples: 1: PlainText; 7: Bash; 8: CSharp; 9: C++; 10: C; 12: CSS; 22: Go; 24: HTML; 29: Java; 30: JavaScript; 32: Kotlin; 43: PHP; 49: Python; 52: Ruby; 53: Rust; 56: SQL; 60: Shell; 61: Swift; 63: TypeScript. Default is 0"),
|
|
223
|
+
wrap: z.boolean().optional().default(false).describe("Whether to enable automatic line wrapping. Default is false"),
|
|
224
|
+
index: z.number().optional().default(0).describe("Insertion position index. Default is 0 (insert at the beginning). If unsure about the position, use the get_feishu_doc_blocks tool first to understand the document structure. For consecutive insertions, calculate the next position as previous_index + 1 to avoid querying document structure repeatedly")
|
|
225
|
+
}, async ({ documentId, parentBlockId, code, language = 0, wrap = false, index = 0 }) => {
|
|
226
|
+
try {
|
|
227
|
+
if (!this.feishuService) {
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
Logger.log(`开始创建飞书代码块,文档ID: ${documentId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
|
|
233
|
+
const result = await this.feishuService.createCodeBlock(documentId, parentBlockId, code, language, wrap, index);
|
|
234
|
+
Logger.log(`飞书代码块创建成功`);
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
Logger.error(`创建飞书代码块失败:`, error);
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: `创建飞书代码块失败: ${error}` }],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
// 添加创建飞书标题块工具
|
|
247
|
+
this.server.tool("create_feishu_heading_block", "Create a heading block in a Feishu document with specified level (1-9)", {
|
|
248
|
+
documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
|
|
249
|
+
parentBlockId: z.string().describe("Parent block ID (NOT URL) where the new block will be added as a child. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter"),
|
|
250
|
+
level: z.number().min(1).max(9).describe("Heading level from 1 to 9, where 1 is the largest heading (h1) and 9 is the smallest (h9)"),
|
|
251
|
+
content: z.string().describe("Heading text content"),
|
|
252
|
+
index: z.number().optional().default(0).describe("Insertion position index. Default is 0 (insert at the beginning). If unsure about the position, use the get_feishu_doc_blocks tool first to understand the document structure. For consecutive insertions, calculate the next position as previous_index + 1 to avoid querying document structure repeatedly")
|
|
253
|
+
}, async ({ documentId, parentBlockId, level, content, index = 0 }) => {
|
|
254
|
+
try {
|
|
255
|
+
if (!this.feishuService) {
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
Logger.log(`开始创建飞书标题块,文档ID: ${documentId},父块ID: ${parentBlockId},标题级别: ${level},插入位置: ${index}`);
|
|
261
|
+
const result = await this.feishuService.createHeadingBlock(documentId, parentBlockId, content, level, index);
|
|
262
|
+
Logger.log(`飞书标题块创建成功`);
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
Logger.error(`创建飞书标题块失败:`, error);
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: `创建飞书标题块失败: ${error}` }],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async connect(transport) {
|
|
276
|
+
// Logger.log("Connecting to transport...");
|
|
277
|
+
await this.server.connect(transport);
|
|
278
|
+
Logger.log = (...args) => {
|
|
279
|
+
this.server.server.sendLoggingMessage({
|
|
280
|
+
level: "info",
|
|
281
|
+
data: args,
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
Logger.error = (...args) => {
|
|
285
|
+
this.server.server.sendLoggingMessage({
|
|
286
|
+
level: "error",
|
|
287
|
+
data: args,
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
Logger.log("Server connected and ready to process requests");
|
|
291
|
+
}
|
|
292
|
+
async startHttpServer(port) {
|
|
293
|
+
const app = express();
|
|
294
|
+
app.get("/sse", async (_req, res) => {
|
|
295
|
+
console.log("New SSE connection established");
|
|
296
|
+
this.sseTransport = new SSEServerTransport("/messages", res);
|
|
297
|
+
await this.server.connect(this.sseTransport);
|
|
298
|
+
});
|
|
299
|
+
app.post("/messages", async (req, res) => {
|
|
300
|
+
if (!this.sseTransport) {
|
|
301
|
+
res.sendStatus(400);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
await this.sseTransport.handlePostMessage(req, res);
|
|
305
|
+
});
|
|
306
|
+
Logger.log = console.log;
|
|
307
|
+
Logger.error = console.error;
|
|
308
|
+
app.listen(port, () => {
|
|
309
|
+
Logger.log(`HTTP server listening on port ${port}`);
|
|
310
|
+
Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
|
|
311
|
+
Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import axios, { AxiosError } from "axios";
|
|
2
|
+
import { Logger } from "../server";
|
|
3
|
+
export class FeishuService {
|
|
4
|
+
constructor(appId, appSecret) {
|
|
5
|
+
Object.defineProperty(this, "appId", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: void 0
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "appSecret", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "baseUrl", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: "https://open.feishu.cn/open-apis"
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "accessToken", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: null
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "tokenExpireTime", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: null
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "MAX_TOKEN_LIFETIME", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: 2 * 60 * 60 * 1000
|
|
40
|
+
}); // 2小时的毫秒数
|
|
41
|
+
this.appId = appId;
|
|
42
|
+
this.appSecret = appSecret;
|
|
43
|
+
}
|
|
44
|
+
isTokenExpired() {
|
|
45
|
+
if (!this.accessToken || !this.tokenExpireTime)
|
|
46
|
+
return true;
|
|
47
|
+
return Date.now() >= this.tokenExpireTime;
|
|
48
|
+
}
|
|
49
|
+
async getAccessToken() {
|
|
50
|
+
if (this.accessToken && !this.isTokenExpired()) {
|
|
51
|
+
Logger.log('使用现有访问令牌,未过期');
|
|
52
|
+
return this.accessToken;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const url = `${this.baseUrl}/auth/v3/tenant_access_token/internal`;
|
|
56
|
+
const requestData = {
|
|
57
|
+
app_id: this.appId,
|
|
58
|
+
app_secret: this.appSecret,
|
|
59
|
+
};
|
|
60
|
+
Logger.log('开始获取新的访问令牌...');
|
|
61
|
+
Logger.log(`请求URL: ${url}`);
|
|
62
|
+
Logger.log(`请求方法: POST`);
|
|
63
|
+
Logger.log(`请求数据: ${JSON.stringify(requestData, null, 2)}`);
|
|
64
|
+
const response = await axios.post(url, requestData);
|
|
65
|
+
Logger.log(`响应状态码: ${response.status}`);
|
|
66
|
+
Logger.log(`响应头: ${JSON.stringify(response.headers, null, 2)}`);
|
|
67
|
+
Logger.log(`响应数据: ${JSON.stringify(response.data, null, 2)}`);
|
|
68
|
+
if (response.data.code !== 0) {
|
|
69
|
+
Logger.error(`获取访问令牌失败,错误码: ${response.data.code}, 错误信息: ${response.data.msg}`);
|
|
70
|
+
throw {
|
|
71
|
+
status: response.status,
|
|
72
|
+
err: response.data.msg || "Unknown error",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
this.accessToken = response.data.tenant_access_token;
|
|
76
|
+
this.tokenExpireTime = Date.now() + Math.min(response.data.expire * 1000, this.MAX_TOKEN_LIFETIME);
|
|
77
|
+
Logger.log(`成功获取新的访问令牌,有效期: ${response.data.expire} 秒`);
|
|
78
|
+
return this.accessToken; // 使用类型断言确保返回类型为string
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof AxiosError && error.response) {
|
|
82
|
+
Logger.error(`获取访问令牌请求失败:`);
|
|
83
|
+
Logger.error(`状态码: ${error.response.status}`);
|
|
84
|
+
Logger.error(`响应头: ${JSON.stringify(error.response.headers, null, 2)}`);
|
|
85
|
+
Logger.error(`响应数据: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
86
|
+
throw {
|
|
87
|
+
status: error.response.status,
|
|
88
|
+
err: error.response.data?.msg || "Unknown error",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
Logger.error('获取访问令牌时发生未知错误:', error);
|
|
92
|
+
throw new Error("Failed to get Feishu access token");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async request(endpoint, method = "GET", data) {
|
|
96
|
+
try {
|
|
97
|
+
const accessToken = await this.getAccessToken();
|
|
98
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
99
|
+
const headers = {
|
|
100
|
+
Authorization: `Bearer ${accessToken}`,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
};
|
|
103
|
+
Logger.log('准备发送请求:');
|
|
104
|
+
Logger.log(`请求URL: ${url}`);
|
|
105
|
+
Logger.log(`请求方法: ${method}`);
|
|
106
|
+
Logger.log(`请求头: ${JSON.stringify(headers, null, 2)}`);
|
|
107
|
+
if (data) {
|
|
108
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
109
|
+
}
|
|
110
|
+
const response = await axios({
|
|
111
|
+
method,
|
|
112
|
+
url,
|
|
113
|
+
headers,
|
|
114
|
+
data,
|
|
115
|
+
});
|
|
116
|
+
Logger.log('收到响应:');
|
|
117
|
+
Logger.log(`响应状态码: ${response.status}`);
|
|
118
|
+
Logger.log(`响应头: ${JSON.stringify(response.headers, null, 2)}`);
|
|
119
|
+
Logger.log(`响应数据: ${JSON.stringify(response.data, null, 2)}`);
|
|
120
|
+
return response.data;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof AxiosError && error.response) {
|
|
124
|
+
Logger.error(`请求失败:`);
|
|
125
|
+
Logger.error(`状态码: ${error.response.status}`);
|
|
126
|
+
Logger.error(`响应头: ${JSON.stringify(error.response.headers, null, 2)}`);
|
|
127
|
+
Logger.error(`响应数据: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
128
|
+
throw {
|
|
129
|
+
status: error.response.status,
|
|
130
|
+
err: error.response.data?.msg || "Unknown error",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
Logger.error('发送请求时发生未知错误:', error);
|
|
134
|
+
throw new Error("Failed to make request to Feishu API");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 创建新文档
|
|
138
|
+
async createDocument(title, folderToken) {
|
|
139
|
+
try {
|
|
140
|
+
Logger.log(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ',根目录'}`);
|
|
141
|
+
const endpoint = '/docx/v1/documents';
|
|
142
|
+
const data = {
|
|
143
|
+
title: title,
|
|
144
|
+
};
|
|
145
|
+
if (folderToken) {
|
|
146
|
+
data.folder_token = folderToken;
|
|
147
|
+
}
|
|
148
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
149
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
150
|
+
const response = await this.request(endpoint, 'POST', data);
|
|
151
|
+
if (response.code !== 0) {
|
|
152
|
+
throw new Error(`创建文档失败: ${response.msg}`);
|
|
153
|
+
}
|
|
154
|
+
const docInfo = response.data?.document;
|
|
155
|
+
Logger.log(`文档创建成功,文档ID: ${docInfo?.document_id}`);
|
|
156
|
+
Logger.log(`文档详情: ${JSON.stringify(docInfo, null, 2)}`);
|
|
157
|
+
return docInfo;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
Logger.error(`创建文档失败:`, error);
|
|
161
|
+
if (error instanceof AxiosError) {
|
|
162
|
+
Logger.error(`请求URL: ${error.config?.url}`);
|
|
163
|
+
Logger.error(`请求方法: ${error.config?.method?.toUpperCase()}`);
|
|
164
|
+
Logger.error(`状态码: ${error.response?.status}`);
|
|
165
|
+
if (error.response?.data) {
|
|
166
|
+
Logger.error(`错误详情: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 获取文档信息
|
|
173
|
+
async getDocumentInfo(documentId) {
|
|
174
|
+
try {
|
|
175
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
176
|
+
if (!docId) {
|
|
177
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
178
|
+
}
|
|
179
|
+
Logger.log(`开始获取文档信息,文档ID: ${docId}`);
|
|
180
|
+
const endpoint = `/docx/v1/documents/${docId}`;
|
|
181
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
182
|
+
const response = await this.request(endpoint);
|
|
183
|
+
if (response.code !== 0) {
|
|
184
|
+
throw new Error(`获取文档信息失败: ${response.msg}`);
|
|
185
|
+
}
|
|
186
|
+
const docInfo = response.data?.document;
|
|
187
|
+
Logger.log(`文档信息获取成功: ${JSON.stringify(docInfo, null, 2)}`);
|
|
188
|
+
if (!docInfo) {
|
|
189
|
+
throw new Error(`获取文档信息失败: 返回的文档信息为空`);
|
|
190
|
+
}
|
|
191
|
+
return docInfo;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
Logger.error(`获取文档信息失败:`, error);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 获取文档纯文本内容
|
|
199
|
+
async getDocumentContent(documentId, lang = 0) {
|
|
200
|
+
try {
|
|
201
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
202
|
+
if (!docId) {
|
|
203
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
204
|
+
}
|
|
205
|
+
Logger.log(`开始获取文档内容,文档ID: ${docId},语言: ${lang}`);
|
|
206
|
+
const endpoint = `/docx/v1/documents/${docId}/raw_content?lang=${lang}`;
|
|
207
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
208
|
+
const response = await this.request(endpoint);
|
|
209
|
+
if (response.code !== 0) {
|
|
210
|
+
throw new Error(`获取文档内容失败: ${response.msg}`);
|
|
211
|
+
}
|
|
212
|
+
Logger.log(`文档内容获取成功,长度: ${response.data?.content?.length || 0}字符`);
|
|
213
|
+
return response.data?.content || '';
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
Logger.error(`获取文档内容失败:`, error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// 获取文档块
|
|
221
|
+
async getDocumentBlocks(documentId, pageSize = 500) {
|
|
222
|
+
try {
|
|
223
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
224
|
+
if (!docId) {
|
|
225
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
226
|
+
}
|
|
227
|
+
Logger.log(`开始获取文档块,文档ID: ${docId},页大小: ${pageSize}`);
|
|
228
|
+
const endpoint = `/docx/v1/documents/${docId}/blocks?document_revision_id=-1&page_size=${pageSize}`;
|
|
229
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
230
|
+
const response = await this.request(endpoint);
|
|
231
|
+
if (response.code !== 0) {
|
|
232
|
+
throw new Error(`获取文档块失败: ${response.msg}`);
|
|
233
|
+
}
|
|
234
|
+
const blocks = response.data?.items || [];
|
|
235
|
+
Logger.log(`文档块获取成功,共 ${blocks.length} 个块`);
|
|
236
|
+
return blocks;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
Logger.error(`获取文档块失败:`, error);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 创建代码块
|
|
244
|
+
async createCodeBlock(documentId, parentBlockId, code, language = 0, wrap = false, index = 0) {
|
|
245
|
+
try {
|
|
246
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
247
|
+
if (!docId) {
|
|
248
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
249
|
+
}
|
|
250
|
+
Logger.log(`开始创建代码块,文档ID: ${docId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
|
|
251
|
+
const blockContent = {
|
|
252
|
+
block_type: 14, // 14表示代码块
|
|
253
|
+
code: {
|
|
254
|
+
elements: [
|
|
255
|
+
{
|
|
256
|
+
text_run: {
|
|
257
|
+
content: code,
|
|
258
|
+
text_element_style: {
|
|
259
|
+
bold: false,
|
|
260
|
+
inline_code: false,
|
|
261
|
+
italic: false,
|
|
262
|
+
strikethrough: false,
|
|
263
|
+
underline: false
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
style: {
|
|
269
|
+
language: language,
|
|
270
|
+
wrap: wrap
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
Logger.log(`代码块内容: ${JSON.stringify(blockContent, null, 2)}`);
|
|
275
|
+
return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
Logger.error(`创建代码块失败:`, error);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// 创建文本块
|
|
283
|
+
async createTextBlock(documentId, parentBlockId, textContents, align = 1, index = 0) {
|
|
284
|
+
try {
|
|
285
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
286
|
+
if (!docId) {
|
|
287
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
288
|
+
}
|
|
289
|
+
Logger.log(`开始创建文本块,文档ID: ${docId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
290
|
+
const blockContent = {
|
|
291
|
+
block_type: 2, // 2表示文本块
|
|
292
|
+
text: {
|
|
293
|
+
elements: textContents.map(content => ({
|
|
294
|
+
text_run: {
|
|
295
|
+
content: content.text,
|
|
296
|
+
text_element_style: content.style || {}
|
|
297
|
+
}
|
|
298
|
+
})),
|
|
299
|
+
style: {
|
|
300
|
+
align: align // 1 居左,2 居中,3 居右
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
Logger.log(`文本块内容: ${JSON.stringify(blockContent, null, 2)}`);
|
|
305
|
+
return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
Logger.error(`创建文本块失败:`, error);
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 创建文档块
|
|
313
|
+
async createDocumentBlock(documentId, parentBlockId, blockContent, index = 0) {
|
|
314
|
+
try {
|
|
315
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
316
|
+
if (!docId) {
|
|
317
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
318
|
+
}
|
|
319
|
+
Logger.log(`开始创建文档块,文档ID: ${docId},父块ID: ${parentBlockId},插入位置: ${index}`);
|
|
320
|
+
const endpoint = `/docx/v1/documents/${docId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
321
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
322
|
+
const data = {
|
|
323
|
+
children: [blockContent],
|
|
324
|
+
index: index
|
|
325
|
+
};
|
|
326
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
327
|
+
const response = await this.request(endpoint, 'POST', data);
|
|
328
|
+
if (response.code !== 0) {
|
|
329
|
+
throw new Error(`创建文档块失败: ${response.msg}`);
|
|
330
|
+
}
|
|
331
|
+
Logger.log(`文档块创建成功: ${JSON.stringify(response.data, null, 2)}`);
|
|
332
|
+
return response.data;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
Logger.error(`创建文档块失败:`, error);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// 创建标题块
|
|
340
|
+
async createHeadingBlock(documentId, parentBlockId, text, level = 1, index = 0) {
|
|
341
|
+
try {
|
|
342
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
343
|
+
if (!docId) {
|
|
344
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
345
|
+
}
|
|
346
|
+
Logger.log(`开始创建标题块,文档ID: ${docId},父块ID: ${parentBlockId},标题级别: ${level},插入位置: ${index}`);
|
|
347
|
+
// 确保标题级别在有效范围内(1-9)
|
|
348
|
+
const safeLevel = Math.max(1, Math.min(9, level));
|
|
349
|
+
// 根据标题级别设置block_type和对应的属性名
|
|
350
|
+
// 飞书API中,一级标题的block_type为3,二级标题为4,以此类推
|
|
351
|
+
const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推
|
|
352
|
+
const headingKey = `heading${safeLevel}`; // heading1, heading2, ...
|
|
353
|
+
// 构建块内容
|
|
354
|
+
const blockContent = {
|
|
355
|
+
block_type: blockType
|
|
356
|
+
};
|
|
357
|
+
// 设置对应级别的标题属性
|
|
358
|
+
blockContent[headingKey] = {
|
|
359
|
+
elements: [
|
|
360
|
+
{
|
|
361
|
+
text_run: {
|
|
362
|
+
content: text,
|
|
363
|
+
text_element_style: {}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
style: {
|
|
368
|
+
align: 1,
|
|
369
|
+
folded: false
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
Logger.log(`标题块内容: ${JSON.stringify(blockContent, null, 2)}`);
|
|
373
|
+
return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
Logger.error(`创建标题块失败:`, error);
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
extractDocIdFromUrl(url) {
|
|
381
|
+
// 处理飞书文档 URL,提取文档 ID
|
|
382
|
+
// 支持多种URL格式
|
|
383
|
+
// 1. 标准文档URL格式: https://xxx.feishu.cn/docs/xxx 或 https://xxx.feishu.cn/docx/xxx
|
|
384
|
+
const docxMatch = url.match(/\/docx\/(\w+)/); // 匹配 docx 格式
|
|
385
|
+
const docsMatch = url.match(/\/docs\/(\w+)/); // 匹配 docs 格式
|
|
386
|
+
// 2. API URL格式: https://open.feishu.cn/open-apis/doc/v2/documents/xxx
|
|
387
|
+
const apiMatch = url.match(/\/documents\/([\w-]+)/); // 匹配 API URL 格式
|
|
388
|
+
// 3. 直接使用文档ID
|
|
389
|
+
const directIdMatch = url.match(/^([\w-]+)$/); // 如果直接传入了文档ID
|
|
390
|
+
// 按优先级返回匹配结果
|
|
391
|
+
return docxMatch ? docxMatch[1] :
|
|
392
|
+
docsMatch ? docsMatch[1] :
|
|
393
|
+
apiMatch ? apiMatch[1] :
|
|
394
|
+
directIdMatch ? directIdMatch[1] : null;
|
|
395
|
+
}
|
|
396
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feishu-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Model Context Protocol server for Feishu integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"feishu-mcp": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && tsc-alias",
|
|
15
|
+
"type-check": "tsc --noEmit",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"start:cli": "cross-env NODE_ENV=cli node dist/index.js",
|
|
18
|
+
"start:http": "node dist/index.js",
|
|
19
|
+
"dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
|
|
20
|
+
"dev:cli": "cross-env NODE_ENV=development tsx watch src/index.ts --stdio",
|
|
21
|
+
"lint": "eslint . --ext .ts",
|
|
22
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
23
|
+
"inspect": "pnpx @modelcontextprotocol/inspector",
|
|
24
|
+
"prepare": "pnpm run build && chmod +x ./dist/cli.js",
|
|
25
|
+
"pub:release": "pnpm build && npm publish"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "^20.17.0"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/cso1z/Feishu-MCP.git"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"feishu",
|
|
36
|
+
"lark",
|
|
37
|
+
"mcp",
|
|
38
|
+
"typescript"
|
|
39
|
+
],
|
|
40
|
+
"author": "cso1z",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
44
|
+
"@types/yargs": "^17.0.33",
|
|
45
|
+
"axios": "^1.7.9",
|
|
46
|
+
"cross-env": "^7.0.3",
|
|
47
|
+
"dotenv": "^16.4.7",
|
|
48
|
+
"express": "^4.21.2",
|
|
49
|
+
"remeda": "^2.20.1",
|
|
50
|
+
"yargs": "^17.7.2",
|
|
51
|
+
"zod": "^3.24.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/express": "^5.0.0",
|
|
55
|
+
"@types/jest": "^29.5.11",
|
|
56
|
+
"@types/node": "^20.17.0",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
|
58
|
+
"@typescript-eslint/parser": "^8.24.0",
|
|
59
|
+
"eslint": "^9.20.1",
|
|
60
|
+
"eslint-config-prettier": "^10.0.1",
|
|
61
|
+
"jest": "^29.7.0",
|
|
62
|
+
"prettier": "^3.5.0",
|
|
63
|
+
"ts-jest": "^29.2.5",
|
|
64
|
+
"tsc-alias": "^1.8.10",
|
|
65
|
+
"tsx": "^4.19.2",
|
|
66
|
+
"typescript": "^5.7.3"
|
|
67
|
+
},
|
|
68
|
+
"pnpm": {
|
|
69
|
+
"overrides": {
|
|
70
|
+
"feishu-mcp": "link:"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|