feishu-mcp 0.0.19 → 0.1.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 -21
- package/README.md +259 -257
- package/dist/mcp/feishuMcp.js +1 -1
- package/dist/mcp/tools/feishuBlockTools.js +33 -0
- package/dist/mcp/tools/feishuTools.js +23 -2
- package/dist/server.js +117 -1
- package/dist/services/blockFactory.js +21 -0
- package/dist/services/feishuApiService.js +66 -9
- package/dist/types/feishuSchema.js +10 -1
- package/dist/utils/document.js +114 -114
- package/dist/utils/paramUtils.js +38 -0
- package/package.json +75 -75
|
@@ -196,7 +196,7 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
// 添加获取画板内容工具
|
|
199
|
-
server.tool('get_feishu_whiteboard_content', 'Retrieves the content and structure of a Feishu whiteboard.
|
|
199
|
+
server.tool('get_feishu_whiteboard_content', 'Retrieves the content and structure of a Feishu whiteboard. Use this to analyze whiteboard content, extract information, or understand the structure of collaborative diagrams. The whiteboard ID can be obtained from the board.token field when getting document blocks with block_type: 43.', {
|
|
200
200
|
whiteboardId: WhiteboardIdSchema,
|
|
201
201
|
}, async ({ whiteboardId }) => {
|
|
202
202
|
try {
|
|
@@ -207,7 +207,28 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
207
207
|
}
|
|
208
208
|
Logger.info(`开始获取飞书画板内容,画板ID: ${whiteboardId}`);
|
|
209
209
|
const whiteboardContent = await feishuService.getWhiteboardContent(whiteboardId);
|
|
210
|
-
|
|
210
|
+
const nodeCount = whiteboardContent.nodes?.length || 0;
|
|
211
|
+
Logger.info(`飞书画板内容获取成功,节点数量: ${nodeCount}`);
|
|
212
|
+
// 检查节点数量是否超过100
|
|
213
|
+
if (nodeCount > 200) {
|
|
214
|
+
Logger.info(`画板节点数量过多 (${nodeCount} > 200),返回缩略图`);
|
|
215
|
+
try {
|
|
216
|
+
const thumbnailBuffer = await feishuService.getWhiteboardThumbnail(whiteboardId);
|
|
217
|
+
const thumbnailBase64 = thumbnailBuffer.toString('base64');
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'image',
|
|
222
|
+
data: thumbnailBase64,
|
|
223
|
+
mimeType: 'image/png'
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (thumbnailError) {
|
|
229
|
+
Logger.warn(`获取画板缩略图失败,返回基本信息: ${thumbnailError}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
211
232
|
return {
|
|
212
233
|
content: [{ type: 'text', text: JSON.stringify(whiteboardContent, null, 2) }],
|
|
213
234
|
};
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
3
6
|
import { Logger } from './utils/logger.js';
|
|
4
7
|
import { SSEConnectionManager } from './manager/sseConnectionManager.js';
|
|
5
8
|
import { FeishuMcp } from './mcp/feishuMcp.js';
|
|
@@ -27,6 +30,118 @@ export class FeishuMcpServer {
|
|
|
27
30
|
}
|
|
28
31
|
async startHttpServer(port) {
|
|
29
32
|
const app = express();
|
|
33
|
+
const transports = {};
|
|
34
|
+
// Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint
|
|
35
|
+
app.use("/mcp", express.json());
|
|
36
|
+
app.post('/mcp', async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
Logger.log("Received StreamableHTTP request", {
|
|
39
|
+
method: req.method,
|
|
40
|
+
url: req.url,
|
|
41
|
+
headers: req.headers,
|
|
42
|
+
body: req.body,
|
|
43
|
+
query: req.query,
|
|
44
|
+
params: req.params
|
|
45
|
+
});
|
|
46
|
+
// Check for existing session ID
|
|
47
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
48
|
+
let transport;
|
|
49
|
+
if (sessionId && transports[sessionId]) {
|
|
50
|
+
// Reuse existing transport
|
|
51
|
+
Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId);
|
|
52
|
+
transport = transports[sessionId];
|
|
53
|
+
}
|
|
54
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
55
|
+
// New initialization request
|
|
56
|
+
transport = new StreamableHTTPServerTransport({
|
|
57
|
+
sessionIdGenerator: () => randomUUID(),
|
|
58
|
+
onsessioninitialized: (sessionId) => {
|
|
59
|
+
// Store the transport by session ID
|
|
60
|
+
Logger.log(`[StreamableHTTP connection] ${sessionId}`);
|
|
61
|
+
transports[sessionId] = transport;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Clean up transport and server when closed
|
|
65
|
+
transport.onclose = () => {
|
|
66
|
+
if (transport.sessionId) {
|
|
67
|
+
Logger.log(`[StreamableHTTP delete] ${transports[transport.sessionId]}`);
|
|
68
|
+
delete transports[transport.sessionId];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// Create and connect server instance
|
|
72
|
+
const server = new FeishuMcp();
|
|
73
|
+
await server.connect(transport);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Invalid request
|
|
77
|
+
res.status(400).json({
|
|
78
|
+
jsonrpc: '2.0',
|
|
79
|
+
error: {
|
|
80
|
+
code: -32000,
|
|
81
|
+
message: 'Bad Request: No valid session ID provided',
|
|
82
|
+
},
|
|
83
|
+
id: null,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Handle the request
|
|
88
|
+
await transport.handleRequest(req, res, req.body);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Error handling MCP request:', error);
|
|
92
|
+
if (!res.headersSent) {
|
|
93
|
+
res.status(500).json({
|
|
94
|
+
jsonrpc: '2.0',
|
|
95
|
+
error: {
|
|
96
|
+
code: -32603,
|
|
97
|
+
message: 'Internal server error',
|
|
98
|
+
},
|
|
99
|
+
id: null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Handle GET requests for server-to-client notifications via Streamable HTTP
|
|
105
|
+
app.get('/mcp', async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
Logger.log("Received StreamableHTTP request get");
|
|
108
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
109
|
+
if (!sessionId || !transports[sessionId]) {
|
|
110
|
+
res.status(400).send('Invalid or missing session ID');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const transport = transports[sessionId];
|
|
114
|
+
await transport.handleRequest(req, res);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Error handling GET request:', error);
|
|
118
|
+
if (!res.headersSent) {
|
|
119
|
+
res.status(500).send('Internal server error');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
// Handle DELETE requests for session termination
|
|
124
|
+
app.delete('/mcp', async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
127
|
+
if (!sessionId || !transports[sessionId]) {
|
|
128
|
+
res.status(400).send('Invalid or missing session ID');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const transport = transports[sessionId];
|
|
132
|
+
await transport.handleRequest(req, res);
|
|
133
|
+
// Clean up resources after session termination
|
|
134
|
+
if (transport.sessionId) {
|
|
135
|
+
delete transports[transport.sessionId];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error('Error handling DELETE request:', error);
|
|
140
|
+
if (!res.headersSent) {
|
|
141
|
+
res.status(500).send('Internal server error');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
30
145
|
app.get('/sse', async (req, res) => {
|
|
31
146
|
const sseTransport = new SSEServerTransport('/messages', res);
|
|
32
147
|
const sessionId = sseTransport.sessionId;
|
|
@@ -82,10 +197,11 @@ export class FeishuMcpServer {
|
|
|
82
197
|
res.status(500).json({ code: 500, msg: e.message || '获取token失败' });
|
|
83
198
|
}
|
|
84
199
|
});
|
|
85
|
-
app.listen(port, () => {
|
|
200
|
+
app.listen(port, '0.0.0.0', () => {
|
|
86
201
|
Logger.info(`HTTP server listening on port ${port}`);
|
|
87
202
|
Logger.info(`SSE endpoint available at http://localhost:${port}/sse`);
|
|
88
203
|
Logger.info(`Message endpoint available at http://localhost:${port}/messages`);
|
|
204
|
+
Logger.info(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`);
|
|
89
205
|
});
|
|
90
206
|
}
|
|
91
207
|
}
|
|
@@ -9,6 +9,7 @@ export var BlockType;
|
|
|
9
9
|
BlockType["HEADING"] = "heading";
|
|
10
10
|
BlockType["LIST"] = "list";
|
|
11
11
|
BlockType["IMAGE"] = "image";
|
|
12
|
+
BlockType["MERMAID"] = "mermaid";
|
|
12
13
|
})(BlockType || (BlockType = {}));
|
|
13
14
|
/**
|
|
14
15
|
* 对齐方式枚举
|
|
@@ -75,6 +76,8 @@ export class BlockFactory {
|
|
|
75
76
|
return this.createListBlock(options);
|
|
76
77
|
case BlockType.IMAGE:
|
|
77
78
|
return this.createImageBlock(options);
|
|
79
|
+
case BlockType.MERMAID:
|
|
80
|
+
return this.createMermaidBlock(options);
|
|
78
81
|
default:
|
|
79
82
|
Logger.error(`不支持的块类型: ${type}`);
|
|
80
83
|
throw new Error(`不支持的块类型: ${type}`);
|
|
@@ -225,4 +228,22 @@ export class BlockFactory {
|
|
|
225
228
|
}
|
|
226
229
|
};
|
|
227
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* 创建Mermaid
|
|
233
|
+
* @param options Mermaid块选项
|
|
234
|
+
* @returns Mermaid块内容对象
|
|
235
|
+
*/
|
|
236
|
+
createMermaidBlock(options = {}) {
|
|
237
|
+
const { code } = options;
|
|
238
|
+
return {
|
|
239
|
+
block_type: 40,
|
|
240
|
+
add_ons: {
|
|
241
|
+
component_id: '',
|
|
242
|
+
component_type_id: 'blk_631fefbbae02400430b8f9f4',
|
|
243
|
+
record: JSON.stringify({
|
|
244
|
+
data: code,
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
228
249
|
}
|
|
@@ -382,6 +382,35 @@ export class FeishuApiService extends BaseApiService {
|
|
|
382
382
|
});
|
|
383
383
|
return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
384
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* 创建Mermaid块
|
|
387
|
+
* @param documentId 文档ID或URL
|
|
388
|
+
* @param parentBlockId 父块ID
|
|
389
|
+
* @param mermaidCode Mermaid代码
|
|
390
|
+
* @param index 插入位置索引
|
|
391
|
+
* @returns 创建结果
|
|
392
|
+
*/
|
|
393
|
+
async createMermaidBlock(documentId, parentBlockId, mermaidCode, index = 0) {
|
|
394
|
+
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
395
|
+
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
396
|
+
const blockContent = {
|
|
397
|
+
block_type: 40,
|
|
398
|
+
add_ons: {
|
|
399
|
+
component_id: "",
|
|
400
|
+
component_type_id: "blk_631fefbbae02400430b8f9f4",
|
|
401
|
+
record: JSON.stringify({
|
|
402
|
+
data: mermaidCode,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const payload = {
|
|
407
|
+
children: [blockContent],
|
|
408
|
+
index
|
|
409
|
+
};
|
|
410
|
+
Logger.info(`请求创建Mermaid块: ${JSON.stringify(payload).slice(0, 500)}...`);
|
|
411
|
+
const response = await this.post(endpoint, payload);
|
|
412
|
+
return response;
|
|
413
|
+
}
|
|
385
414
|
/**
|
|
386
415
|
* 删除文档中的块,支持批量删除
|
|
387
416
|
* @param documentId 文档ID或URL
|
|
@@ -577,6 +606,14 @@ export class FeishuApiService extends BaseApiService {
|
|
|
577
606
|
};
|
|
578
607
|
}
|
|
579
608
|
break;
|
|
609
|
+
case BlockType.MERMAID:
|
|
610
|
+
if ('mermaid' in options && options.mermaid) {
|
|
611
|
+
const mermaidOptions = options.mermaid;
|
|
612
|
+
blockConfig.options = {
|
|
613
|
+
code: mermaidOptions.code,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
580
617
|
default:
|
|
581
618
|
Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
|
|
582
619
|
if ('text' in options) {
|
|
@@ -640,6 +677,13 @@ export class FeishuApiService extends BaseApiService {
|
|
|
640
677
|
height: imageOptions.height || 100
|
|
641
678
|
};
|
|
642
679
|
}
|
|
680
|
+
else if ("mermaid" in options) {
|
|
681
|
+
blockConfig.type = BlockType.MERMAID;
|
|
682
|
+
const mermaidConfig = options.mermaid;
|
|
683
|
+
blockConfig.options = {
|
|
684
|
+
code: mermaidConfig.code,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
643
687
|
break;
|
|
644
688
|
}
|
|
645
689
|
// 记录调试信息
|
|
@@ -955,15 +999,7 @@ export class FeishuApiService extends BaseApiService {
|
|
|
955
999
|
*/
|
|
956
1000
|
async getWhiteboardContent(whiteboardId) {
|
|
957
1001
|
try {
|
|
958
|
-
|
|
959
|
-
let normalizedWhiteboardId = whiteboardId;
|
|
960
|
-
if (whiteboardId.includes('feishu.cn/board/')) {
|
|
961
|
-
// 从URL中提取画板ID
|
|
962
|
-
const matches = whiteboardId.match(/board\/([^\/\?]+)/);
|
|
963
|
-
if (matches) {
|
|
964
|
-
normalizedWhiteboardId = matches[1];
|
|
965
|
-
}
|
|
966
|
-
}
|
|
1002
|
+
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
967
1003
|
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes`;
|
|
968
1004
|
Logger.info(`开始获取画板内容,画板ID: ${normalizedWhiteboardId}`);
|
|
969
1005
|
const response = await this.get(endpoint);
|
|
@@ -974,6 +1010,27 @@ export class FeishuApiService extends BaseApiService {
|
|
|
974
1010
|
this.handleApiError(error, '获取画板内容失败');
|
|
975
1011
|
}
|
|
976
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* 获取画板缩略图
|
|
1015
|
+
* @param whiteboardId 画板ID或URL
|
|
1016
|
+
* @returns 画板缩略图的二进制数据
|
|
1017
|
+
*/
|
|
1018
|
+
async getWhiteboardThumbnail(whiteboardId) {
|
|
1019
|
+
try {
|
|
1020
|
+
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
1021
|
+
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/download_as_image`;
|
|
1022
|
+
Logger.info(`开始获取画板缩略图,画板ID: ${normalizedWhiteboardId}`);
|
|
1023
|
+
// 使用通用的request方法获取二进制响应
|
|
1024
|
+
const response = await this.request(endpoint, 'GET', {}, true, {}, 'arraybuffer');
|
|
1025
|
+
const thumbnailBuffer = Buffer.from(response);
|
|
1026
|
+
Logger.info(`画板缩略图获取成功,大小: ${thumbnailBuffer.length} 字节`);
|
|
1027
|
+
return thumbnailBuffer;
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
this.handleApiError(error, '获取画板缩略图失败');
|
|
1031
|
+
return Buffer.from([]); // 永远不会执行到这里
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
977
1034
|
/**
|
|
978
1035
|
* 从路径或URL获取图片的Base64编码
|
|
979
1036
|
* @param imagePathOrUrl 图片路径或URL
|
|
@@ -103,7 +103,7 @@ export const ListBlockSchema = z.object({
|
|
|
103
103
|
align: AlignSchemaWithValidation,
|
|
104
104
|
});
|
|
105
105
|
// 块类型枚举 - 用于批量创建块工具
|
|
106
|
-
export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image',
|
|
106
|
+
export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image','mermaid',as well as 'heading1' through 'heading9'. " +
|
|
107
107
|
"For headings, we recommend using 'heading' with level property, but 'heading1'-'heading9' are also supported. " +
|
|
108
108
|
"For images, use 'image' to create empty image blocks that can be filled later. " +
|
|
109
109
|
"For text blocks, you can include both regular text and equation elements in the same block.");
|
|
@@ -116,6 +116,14 @@ export const ImageBlockSchema = z.object({
|
|
|
116
116
|
width: ImageWidthSchema,
|
|
117
117
|
height: ImageHeightSchema
|
|
118
118
|
});
|
|
119
|
+
// Mermaid代码参数定义
|
|
120
|
+
export const MermaidCodeSchema = z.string().describe('Mermaid code (required). The complete Mermaid chart code, e.g. \'graph TD; A-->B;\'. ' +
|
|
121
|
+
'IMPORTANT: When node text contains special characters like parentheses (), brackets [], or arrows -->, ' +
|
|
122
|
+
'wrap the entire text in double quotes to prevent parsing errors. ' +
|
|
123
|
+
'Example: A["finish()/返回键"] instead of A[finish()/返回键].');
|
|
124
|
+
export const MermaidBlockSchema = z.object({
|
|
125
|
+
code: MermaidCodeSchema,
|
|
126
|
+
});
|
|
119
127
|
// 块配置定义 - 用于批量创建块工具
|
|
120
128
|
export const BlockConfigSchema = z.object({
|
|
121
129
|
blockType: BlockTypeEnum,
|
|
@@ -125,6 +133,7 @@ export const BlockConfigSchema = z.object({
|
|
|
125
133
|
z.object({ heading: HeadingBlockSchema }).describe("Heading block options. Used with both 'heading' and 'headingN' formats."),
|
|
126
134
|
z.object({ list: ListBlockSchema }).describe("List block options. Used when blockType is 'list'."),
|
|
127
135
|
z.object({ image: ImageBlockSchema }).describe("Image block options. Used when blockType is 'image'. Creates empty image blocks."),
|
|
136
|
+
z.object({ mermaid: MermaidBlockSchema }).describe("Mermaid block options. Used when blockType is 'mermaid'."),
|
|
128
137
|
z.record(z.any()).describe("Fallback for any other block options")
|
|
129
138
|
]).describe('Options for the specific block type. Provide the corresponding options object based on blockType.'),
|
|
130
139
|
});
|
package/dist/utils/document.js
CHANGED
|
@@ -136,127 +136,127 @@ export function renderFeishuAuthResultHtml(data) {
|
|
|
136
136
|
expiresIn = expiresIn - now;
|
|
137
137
|
if (refreshExpiresIn && refreshExpiresIn > 1000000000)
|
|
138
138
|
refreshExpiresIn = refreshExpiresIn - now;
|
|
139
|
-
const tokenBlock = data && !isError ? `
|
|
140
|
-
<div class="card">
|
|
141
|
-
<h3>Token 信息</h3>
|
|
142
|
-
<ul class="kv-list">
|
|
143
|
-
<li><b>token_type:</b> <span>${data.token_type || ''}</span></li>
|
|
144
|
-
<li><b>access_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.access_token || ''}</pre></li>
|
|
145
|
-
<li><b>expires_in:</b> <span>${formatExpire(expiresIn)}</span></li>
|
|
146
|
-
<li><b>refresh_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.refresh_token || ''}</pre></li>
|
|
147
|
-
<li><b>refresh_token_expires_in:</b> <span>${formatExpire(refreshExpiresIn)}</span></li>
|
|
148
|
-
<li><b>scope:</b> <pre class="scope">${(data.scope || '').replace(/ /g, '\n')}</pre></li>
|
|
149
|
-
</ul>
|
|
150
|
-
<div class="success-action">
|
|
151
|
-
<span class="success-msg">授权成功,继续完成任务</span>
|
|
152
|
-
<button class="copy-btn" onclick="copySuccessMsg(this)">点击复制到粘贴板</button>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
139
|
+
const tokenBlock = data && !isError ? `
|
|
140
|
+
<div class="card">
|
|
141
|
+
<h3>Token 信息</h3>
|
|
142
|
+
<ul class="kv-list">
|
|
143
|
+
<li><b>token_type:</b> <span>${data.token_type || ''}</span></li>
|
|
144
|
+
<li><b>access_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.access_token || ''}</pre></li>
|
|
145
|
+
<li><b>expires_in:</b> <span>${formatExpire(expiresIn)}</span></li>
|
|
146
|
+
<li><b>refresh_token:</b> <span class="foldable" onclick="toggleFold(this)">点击展开/收起</span><pre class="fold scrollable">${data.refresh_token || ''}</pre></li>
|
|
147
|
+
<li><b>refresh_token_expires_in:</b> <span>${formatExpire(refreshExpiresIn)}</span></li>
|
|
148
|
+
<li><b>scope:</b> <pre class="scope">${(data.scope || '').replace(/ /g, '\n')}</pre></li>
|
|
149
|
+
</ul>
|
|
150
|
+
<div class="success-action">
|
|
151
|
+
<span class="success-msg">授权成功,继续完成任务</span>
|
|
152
|
+
<button class="copy-btn" onclick="copySuccessMsg(this)">点击复制到粘贴板</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
155
|
` : '';
|
|
156
156
|
let userBlock = '';
|
|
157
157
|
const userInfo = data && data.userInfo && data.userInfo.data;
|
|
158
158
|
if (userInfo) {
|
|
159
|
-
userBlock = `
|
|
160
|
-
<div class="card user-card">
|
|
161
|
-
<div class="avatar-wrap">
|
|
162
|
-
<img src="${userInfo.avatar_big || userInfo.avatar_thumb || userInfo.avatar_url || ''}" class="avatar" />
|
|
163
|
-
</div>
|
|
164
|
-
<div class="user-info">
|
|
165
|
-
<div class="user-name">${userInfo.name || ''}</div>
|
|
166
|
-
<div class="user-en">${userInfo.en_name || ''}</div>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
159
|
+
userBlock = `
|
|
160
|
+
<div class="card user-card">
|
|
161
|
+
<div class="avatar-wrap">
|
|
162
|
+
<img src="${userInfo.avatar_big || userInfo.avatar_thumb || userInfo.avatar_url || ''}" class="avatar" />
|
|
163
|
+
</div>
|
|
164
|
+
<div class="user-info">
|
|
165
|
+
<div class="user-name">${userInfo.name || ''}</div>
|
|
166
|
+
<div class="user-en">${userInfo.en_name || ''}</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
169
|
`;
|
|
170
170
|
}
|
|
171
|
-
const errorBlock = isError ? `
|
|
172
|
-
<div class="card error-card">
|
|
173
|
-
<h3>授权失败</h3>
|
|
174
|
-
<div class="error-msg">${escapeHtml(data.error || '')}</div>
|
|
175
|
-
<div class="error-code">错误码: ${data.code || ''}</div>
|
|
176
|
-
</div>
|
|
171
|
+
const errorBlock = isError ? `
|
|
172
|
+
<div class="card error-card">
|
|
173
|
+
<h3>授权失败</h3>
|
|
174
|
+
<div class="error-msg">${escapeHtml(data.error || '')}</div>
|
|
175
|
+
<div class="error-code">错误码: ${data.code || ''}</div>
|
|
176
|
+
</div>
|
|
177
177
|
` : '';
|
|
178
|
-
return `
|
|
179
|
-
<html>
|
|
180
|
-
<head>
|
|
181
|
-
<title>飞书授权结果</title>
|
|
182
|
-
<meta charset="utf-8"/>
|
|
183
|
-
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
184
|
-
<style>
|
|
185
|
-
body { background: #f7f8fa; font-family: 'Segoe UI', Arial, sans-serif; margin:0; padding:0; }
|
|
186
|
-
.container { max-width: 600px; margin: 40px auto; padding: 16px; }
|
|
187
|
-
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px #0001; margin-bottom: 24px; padding: 24px 20px; }
|
|
188
|
-
.user-card { display: flex; align-items: center; gap: 24px; }
|
|
189
|
-
.avatar-wrap { flex-shrink: 0; }
|
|
190
|
-
.avatar { width: 96px; height: 96px; border-radius: 50%; box-shadow: 0 2px 8px #0002; display: block; margin: 0 auto; }
|
|
191
|
-
.user-info { flex: 1; }
|
|
192
|
-
.user-name { font-size: 1.5em; font-weight: bold; margin-bottom: 4px; }
|
|
193
|
-
.user-en { color: #888; margin-bottom: 10px; }
|
|
194
|
-
.kv-list { list-style: none; padding: 0; margin: 0; }
|
|
195
|
-
.kv-list li { margin-bottom: 6px; word-break: break-all; }
|
|
196
|
-
.kv-list b { color: #1976d2; }
|
|
197
|
-
.scope { background: #f0f4f8; border-radius: 4px; padding: 6px; font-size: 0.95em; white-space: pre-line; }
|
|
198
|
-
.foldable { color: #1976d2; cursor: pointer; text-decoration: underline; margin-left: 8px; }
|
|
199
|
-
.fold { display: none; background: #f6f6f6; border-radius: 4px; padding: 6px; margin: 4px 0; font-size: 0.92em; max-width: 100%; overflow-x: auto; word-break: break-all; }
|
|
200
|
-
.scrollable { max-width: 100%; overflow-x: auto; font-family: 'Fira Mono', 'Consolas', 'Menlo', monospace; font-size: 0.93em; }
|
|
201
|
-
.success-action { margin-top: 18px; display: flex; align-items: center; gap: 16px; }
|
|
202
|
-
.success-msg { color: #388e3c; font-weight: bold; }
|
|
203
|
-
.copy-btn { background: #1976d2; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; font-size: 1em; cursor: pointer; transition: background 0.2s; }
|
|
204
|
-
.copy-btn:hover { background: #125ea2; }
|
|
205
|
-
.error-card { border-left: 6px solid #e53935; background: #fff0f0; color: #b71c1c; }
|
|
206
|
-
.error-msg { font-size: 1.1em; margin-bottom: 8px; }
|
|
207
|
-
.error-code { color: #b71c1c; font-size: 0.95em; }
|
|
208
|
-
.raw-block { margin-top: 24px; }
|
|
209
|
-
.raw-toggle { color: #1976d2; cursor: pointer; text-decoration: underline; margin-bottom: 8px; display: inline-block; }
|
|
210
|
-
.raw-pre { display: none; background: #23272e; color: #fff; border-radius: 6px; padding: 12px; font-size: 0.95em; overflow-x: auto; max-width: 100%; }
|
|
211
|
-
@media (max-width: 700px) {
|
|
212
|
-
.container { max-width: 98vw; padding: 4vw; }
|
|
213
|
-
.card { padding: 4vw 3vw; }
|
|
214
|
-
.avatar { width: 64px; height: 64px; }
|
|
215
|
-
}
|
|
216
|
-
</style>
|
|
217
|
-
<script>
|
|
218
|
-
function toggleFold(el) {
|
|
219
|
-
var pre = el.nextElementSibling;
|
|
220
|
-
if (pre.style.display === 'block') {
|
|
221
|
-
pre.style.display = 'none';
|
|
222
|
-
} else {
|
|
223
|
-
pre.style.display = 'block';
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function toggleRaw() {
|
|
227
|
-
var pre = document.getElementById('raw-pre');
|
|
228
|
-
if (pre.style.display === 'block') {
|
|
229
|
-
pre.style.display = 'none';
|
|
230
|
-
} else {
|
|
231
|
-
pre.style.display = 'block';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function copySuccessMsg(btn) {
|
|
235
|
-
var text = '授权成功,继续完成任务';
|
|
236
|
-
navigator.clipboard.writeText(text).then(function() {
|
|
237
|
-
btn.innerText = '已复制';
|
|
238
|
-
btn.disabled = true;
|
|
239
|
-
setTimeout(function() {
|
|
240
|
-
btn.innerText = '点击复制到粘贴板';
|
|
241
|
-
btn.disabled = false;
|
|
242
|
-
}, 2000);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
</script>
|
|
246
|
-
</head>
|
|
247
|
-
<body>
|
|
248
|
-
<div class="container">
|
|
249
|
-
<h2 style="margin-bottom:24px;">飞书授权结果</h2>
|
|
250
|
-
${errorBlock}
|
|
251
|
-
${tokenBlock}
|
|
252
|
-
${userBlock}
|
|
253
|
-
<div class="card raw-block">
|
|
254
|
-
<span class="raw-toggle" onclick="toggleRaw()">点击展开/收起原始数据</span>
|
|
255
|
-
<pre id="raw-pre" class="raw-pre">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
</body>
|
|
259
|
-
</html>
|
|
178
|
+
return `
|
|
179
|
+
<html>
|
|
180
|
+
<head>
|
|
181
|
+
<title>飞书授权结果</title>
|
|
182
|
+
<meta charset="utf-8"/>
|
|
183
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
184
|
+
<style>
|
|
185
|
+
body { background: #f7f8fa; font-family: 'Segoe UI', Arial, sans-serif; margin:0; padding:0; }
|
|
186
|
+
.container { max-width: 600px; margin: 40px auto; padding: 16px; }
|
|
187
|
+
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px #0001; margin-bottom: 24px; padding: 24px 20px; }
|
|
188
|
+
.user-card { display: flex; align-items: center; gap: 24px; }
|
|
189
|
+
.avatar-wrap { flex-shrink: 0; }
|
|
190
|
+
.avatar { width: 96px; height: 96px; border-radius: 50%; box-shadow: 0 2px 8px #0002; display: block; margin: 0 auto; }
|
|
191
|
+
.user-info { flex: 1; }
|
|
192
|
+
.user-name { font-size: 1.5em; font-weight: bold; margin-bottom: 4px; }
|
|
193
|
+
.user-en { color: #888; margin-bottom: 10px; }
|
|
194
|
+
.kv-list { list-style: none; padding: 0; margin: 0; }
|
|
195
|
+
.kv-list li { margin-bottom: 6px; word-break: break-all; }
|
|
196
|
+
.kv-list b { color: #1976d2; }
|
|
197
|
+
.scope { background: #f0f4f8; border-radius: 4px; padding: 6px; font-size: 0.95em; white-space: pre-line; }
|
|
198
|
+
.foldable { color: #1976d2; cursor: pointer; text-decoration: underline; margin-left: 8px; }
|
|
199
|
+
.fold { display: none; background: #f6f6f6; border-radius: 4px; padding: 6px; margin: 4px 0; font-size: 0.92em; max-width: 100%; overflow-x: auto; word-break: break-all; }
|
|
200
|
+
.scrollable { max-width: 100%; overflow-x: auto; font-family: 'Fira Mono', 'Consolas', 'Menlo', monospace; font-size: 0.93em; }
|
|
201
|
+
.success-action { margin-top: 18px; display: flex; align-items: center; gap: 16px; }
|
|
202
|
+
.success-msg { color: #388e3c; font-weight: bold; }
|
|
203
|
+
.copy-btn { background: #1976d2; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; font-size: 1em; cursor: pointer; transition: background 0.2s; }
|
|
204
|
+
.copy-btn:hover { background: #125ea2; }
|
|
205
|
+
.error-card { border-left: 6px solid #e53935; background: #fff0f0; color: #b71c1c; }
|
|
206
|
+
.error-msg { font-size: 1.1em; margin-bottom: 8px; }
|
|
207
|
+
.error-code { color: #b71c1c; font-size: 0.95em; }
|
|
208
|
+
.raw-block { margin-top: 24px; }
|
|
209
|
+
.raw-toggle { color: #1976d2; cursor: pointer; text-decoration: underline; margin-bottom: 8px; display: inline-block; }
|
|
210
|
+
.raw-pre { display: none; background: #23272e; color: #fff; border-radius: 6px; padding: 12px; font-size: 0.95em; overflow-x: auto; max-width: 100%; }
|
|
211
|
+
@media (max-width: 700px) {
|
|
212
|
+
.container { max-width: 98vw; padding: 4vw; }
|
|
213
|
+
.card { padding: 4vw 3vw; }
|
|
214
|
+
.avatar { width: 64px; height: 64px; }
|
|
215
|
+
}
|
|
216
|
+
</style>
|
|
217
|
+
<script>
|
|
218
|
+
function toggleFold(el) {
|
|
219
|
+
var pre = el.nextElementSibling;
|
|
220
|
+
if (pre.style.display === 'block') {
|
|
221
|
+
pre.style.display = 'none';
|
|
222
|
+
} else {
|
|
223
|
+
pre.style.display = 'block';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function toggleRaw() {
|
|
227
|
+
var pre = document.getElementById('raw-pre');
|
|
228
|
+
if (pre.style.display === 'block') {
|
|
229
|
+
pre.style.display = 'none';
|
|
230
|
+
} else {
|
|
231
|
+
pre.style.display = 'block';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function copySuccessMsg(btn) {
|
|
235
|
+
var text = '授权成功,继续完成任务';
|
|
236
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
237
|
+
btn.innerText = '已复制';
|
|
238
|
+
btn.disabled = true;
|
|
239
|
+
setTimeout(function() {
|
|
240
|
+
btn.innerText = '点击复制到粘贴板';
|
|
241
|
+
btn.disabled = false;
|
|
242
|
+
}, 2000);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
</script>
|
|
246
|
+
</head>
|
|
247
|
+
<body>
|
|
248
|
+
<div class="container">
|
|
249
|
+
<h2 style="margin-bottom:24px;">飞书授权结果</h2>
|
|
250
|
+
${errorBlock}
|
|
251
|
+
${tokenBlock}
|
|
252
|
+
${userBlock}
|
|
253
|
+
<div class="card raw-block">
|
|
254
|
+
<span class="raw-toggle" onclick="toggleRaw()">点击展开/收起原始数据</span>
|
|
255
|
+
<pre id="raw-pre" class="raw-pre">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</body>
|
|
259
|
+
</html>
|
|
260
260
|
`;
|
|
261
261
|
}
|
|
262
262
|
function escapeHtml(str) {
|
package/dist/utils/paramUtils.js
CHANGED
|
@@ -159,6 +159,44 @@ export class ParamUtils {
|
|
|
159
159
|
// 限制在1-9范围内
|
|
160
160
|
return Math.max(1, Math.min(9, level));
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* 处理画板ID参数
|
|
164
|
+
* 验证并规范化画板ID,支持从URL中提取
|
|
165
|
+
*
|
|
166
|
+
* @param whiteboardId 画板ID或URL
|
|
167
|
+
* @returns 规范化的画板ID
|
|
168
|
+
* @throws 如果画板ID无效则抛出错误
|
|
169
|
+
*/
|
|
170
|
+
static processWhiteboardId(whiteboardId) {
|
|
171
|
+
if (!whiteboardId) {
|
|
172
|
+
throw new ParamValidationError('whiteboardId', '画板ID不能为空');
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
// 从URL中提取画板ID
|
|
176
|
+
let normalizedWhiteboardId = whiteboardId;
|
|
177
|
+
if (whiteboardId.includes('feishu.cn/board/')) {
|
|
178
|
+
// 从URL中提取画板ID
|
|
179
|
+
const matches = whiteboardId.match(/board\/([^\/\?]+)/);
|
|
180
|
+
if (matches) {
|
|
181
|
+
normalizedWhiteboardId = matches[1];
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new ParamValidationError('whiteboardId', '无法从URL中提取画板ID');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// 验证画板ID格式(基本格式检查)
|
|
188
|
+
if (!/^[a-zA-Z0-9_-]{5,}$/.test(normalizedWhiteboardId)) {
|
|
189
|
+
throw new ParamValidationError('whiteboardId', '画板ID格式无效');
|
|
190
|
+
}
|
|
191
|
+
return normalizedWhiteboardId;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof ParamValidationError) {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
throw new ParamValidationError('whiteboardId', formatErrorMessage(error));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
162
200
|
/**
|
|
163
201
|
* 批量处理通用参数
|
|
164
202
|
* 验证并规范化常用参数集
|