dingtalk-wiki 1.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/index.js ADDED
@@ -0,0 +1,944 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DingTalk Wiki MCP Server
4
+ * 钉钉知识库 MCP 服务 - 支持读写操作
5
+ *
6
+ * 基于钉钉 Wiki API v2.0
7
+ * 文档: https://open.dingtalk.com/document/development/knowledge-base-overview
8
+ */
9
+
10
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
11
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
12
+ const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
13
+ const axios = require('axios');
14
+ const dotenv = require('dotenv');
15
+
16
+ // 钉钉 API 配置
17
+ const DINGTALK_API_BASE = 'https://oapi.dingtalk.com';
18
+ const DINGTALK_API_V2 = 'https://api.dingtalk.com';
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+ const CACHE_DIR = path.join(os.homedir(), '.cache', 'dingtalk-wiki-mcp');
23
+ const UNIONID_CACHE_PATH = path.join(CACHE_DIR, 'unionid-cache.json');
24
+
25
+ if (!fs.existsSync(CACHE_DIR)) {
26
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
27
+ }
28
+
29
+ let unionIdCache = {};
30
+ try {
31
+ if (fs.existsSync(UNIONID_CACHE_PATH)) {
32
+ unionIdCache = JSON.parse(fs.readFileSync(UNIONID_CACHE_PATH, 'utf8'));
33
+ }
34
+ } catch (e) {
35
+ console.error('[钉钉MCP] 读取 unionId 缓存失败:', e.message);
36
+ }
37
+
38
+ function saveUnionIdCache() {
39
+ try {
40
+ fs.writeFileSync(UNIONID_CACHE_PATH, JSON.stringify(unionIdCache, null, 2), 'utf8');
41
+ } catch (e) {
42
+ console.error('[钉钉MCP] 写入 unionId 缓存失败:', e.message);
43
+ }
44
+ }
45
+
46
+ function loadEnvFile(filePath) {
47
+ if (!filePath || !fs.existsSync(filePath)) {
48
+ return false;
49
+ }
50
+
51
+ dotenv.config({ path: filePath, override: false });
52
+ return true;
53
+ }
54
+
55
+ const DOTENV_CANDIDATES = [
56
+ process.env.DINGTALK_WIKI_ENV_PATH,
57
+ path.join(process.cwd(), '.env'),
58
+ path.join(__dirname, '.env')
59
+ ].filter(Boolean);
60
+
61
+ for (const candidate of DOTENV_CANDIDATES) {
62
+ if (loadEnvFile(candidate)) {
63
+ console.error(`[钉钉MCP] 已加载环境变量文件: ${candidate}`);
64
+ break;
65
+ }
66
+ }
67
+
68
+ // 加载配置
69
+ let userConfig = {};
70
+ if (process.env.DINGTALK_WIKI_CONFIG) {
71
+ try {
72
+ userConfig = JSON.parse(process.env.DINGTALK_WIKI_CONFIG);
73
+ console.error('[钉钉MCP] 已加载环境变量配置');
74
+ } catch (error) {
75
+ console.error('[钉钉MCP] 环境变量配置解析失败:', error.message);
76
+ }
77
+ }
78
+
79
+ // 环境变量配置
80
+ const DINGTALK_APP_KEY = process.env.DINGTALK_APP_KEY;
81
+ const DINGTALK_APP_SECRET = process.env.DINGTALK_APP_SECRET;
82
+
83
+ if (!DINGTALK_APP_KEY || !DINGTALK_APP_SECRET) {
84
+ console.error('错误: 请设置环境变量 DINGTALK_APP_KEY 和 DINGTALK_APP_SECRET');
85
+ process.exit(1);
86
+ }
87
+
88
+ // 钉钉 API 客户端
89
+ class DingTalkClient {
90
+ constructor() {
91
+ this.accessToken = null;
92
+ this.tokenExpireTime = 0;
93
+ this.operatorId = null;
94
+ }
95
+
96
+ // 获取 Access Token
97
+ async getAccessToken() {
98
+ if (this.accessToken && Date.now() < this.tokenExpireTime) {
99
+ return this.accessToken;
100
+ }
101
+
102
+ try {
103
+ const response = await axios.get(`${DINGTALK_API_BASE}/gettoken`, {
104
+ params: {
105
+ appkey: DINGTALK_APP_KEY,
106
+ appsecret: DINGTALK_APP_SECRET
107
+ }
108
+ });
109
+
110
+ if (response.data.errcode !== 0) {
111
+ throw new Error(`获取 Token 失败: ${response.data.errmsg}`);
112
+ }
113
+
114
+ this.accessToken = response.data.access_token;
115
+ // Token 7200 秒过期,提前 5 分钟刷新
116
+ this.tokenExpireTime = Date.now() + (response.data.expires_in - 300) * 1000;
117
+ return this.accessToken;
118
+ } catch (error) {
119
+ throw new Error(`获取 Access Token 失败: ${error.message}`);
120
+ }
121
+ }
122
+
123
+ // 设置操作者 ID (unionid)
124
+ setOperatorId(unionid) {
125
+ this.operatorId = unionid;
126
+ console.error(`[钉钉MCP] 操作者已设置: ${unionid}`);
127
+ return true;
128
+ }
129
+
130
+ // 获取默认用户的 unionId — 优先级:内存 > 系统缓存 > API
131
+ async getCurrentUserUnionId() {
132
+ if (this.operatorId) {
133
+ return this.operatorId;
134
+ }
135
+
136
+ const defaultUser = userConfig.defaultUser;
137
+ const users = userConfig.users;
138
+ if (!defaultUser || !users || !users[defaultUser]) {
139
+ return null;
140
+ }
141
+
142
+ const user = users[defaultUser];
143
+ if (!user.userId) {
144
+ return null;
145
+ }
146
+
147
+ // 查系统缓存
148
+ if (unionIdCache[user.userId]) {
149
+ this.operatorId = unionIdCache[user.userId];
150
+ return this.operatorId;
151
+ }
152
+
153
+ // 调 API
154
+ try {
155
+ const token = await this.getAccessToken();
156
+ const response = await axios({
157
+ method: 'POST',
158
+ url: `${DINGTALK_API_BASE}/topapi/v2/user/get`,
159
+ params: { access_token: token },
160
+ data: { userid: user.userId }
161
+ });
162
+ if (response.data.errcode === 0 && response.data.result && response.data.result.unionid) {
163
+ const unionid = response.data.result.unionid;
164
+ this.operatorId = unionid;
165
+ unionIdCache[user.userId] = unionid;
166
+ saveUnionIdCache();
167
+ return unionid;
168
+ }
169
+ } catch (error) {
170
+ console.error('[钉钉MCP] 获取 unionId 失败:', error.message);
171
+ }
172
+ return null;
173
+ }
174
+
175
+ // Wiki API v2.0 请求
176
+ async wikiRequest(endpoint, params = {}) {
177
+ const token = await this.getAccessToken();
178
+
179
+ if (!this.operatorId) {
180
+ await this.getCurrentUserUnionId();
181
+ }
182
+
183
+ const queryParams = new URLSearchParams();
184
+ for (const [key, value] of Object.entries(params)) {
185
+ if (value !== undefined && value !== null) {
186
+ queryParams.append(key, value);
187
+ }
188
+ }
189
+
190
+ // 添加 operatorId
191
+ if (this.operatorId) {
192
+ queryParams.append('operatorId', this.operatorId);
193
+ }
194
+
195
+ const url = `${DINGTALK_API_V2}/v2.0/wiki/${endpoint}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
196
+
197
+ try {
198
+ const response = await axios({
199
+ method: 'GET',
200
+ url,
201
+ headers: {
202
+ 'x-acs-dingtalk-access-token': token
203
+ }
204
+ });
205
+ return response.data;
206
+ } catch (error) {
207
+ if (error.response) {
208
+ throw new Error(`${error.response.data?.message || error.message} (code: ${error.response.data?.code})`);
209
+ }
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ async resolveOperatorId(overrideOperatorId = null) {
215
+ if (overrideOperatorId) {
216
+ this.setOperatorId(overrideOperatorId);
217
+ return this.operatorId;
218
+ }
219
+
220
+ if (!this.operatorId) {
221
+ await this.getCurrentUserUnionId();
222
+ }
223
+
224
+ if (!this.operatorId) {
225
+ throw new Error('未设置 operator_id,请传入 operator_id 或在配置文件中设置默认用户');
226
+ }
227
+
228
+ return this.operatorId;
229
+ }
230
+
231
+ async notableRequest(method, pathName, { operatorId = null, params = {}, data = null } = {}) {
232
+ const token = await this.getAccessToken();
233
+ const resolvedOperatorId = await this.resolveOperatorId(operatorId);
234
+ const url = `${DINGTALK_API_V2}${pathName}`;
235
+
236
+ try {
237
+ const response = await axios({
238
+ method,
239
+ url,
240
+ headers: {
241
+ 'x-acs-dingtalk-access-token': token,
242
+ 'Content-Type': 'application/json'
243
+ },
244
+ params: {
245
+ ...params,
246
+ operatorId: resolvedOperatorId
247
+ },
248
+ data
249
+ });
250
+ return response.data;
251
+ } catch (error) {
252
+ if (error.response) {
253
+ throw new Error(`${error.response.data?.message || error.message} (code: ${error.response.data?.code})`);
254
+ }
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ // OAPI 请求
260
+ async oapiRequest(apiName, data = null) {
261
+ const token = await this.getAccessToken();
262
+ const url = `${DINGTALK_API_BASE}/topapi/${apiName}`;
263
+
264
+ const config = {
265
+ method: data ? 'POST' : 'GET',
266
+ url,
267
+ headers: {
268
+ 'Content-Type': 'application/json'
269
+ }
270
+ };
271
+
272
+ if (data) {
273
+ config.data = data;
274
+ config.params = { access_token: token };
275
+ } else {
276
+ config.params = { access_token: token };
277
+ }
278
+
279
+ try {
280
+ const response = await axios(config);
281
+ if (response.data.errcode !== 0) {
282
+ throw new Error(`${response.data.errmsg || '未知错误'} (错误码: ${response.data.errcode})`);
283
+ }
284
+ return response.data;
285
+ } catch (error) {
286
+ if (error.response) {
287
+ throw new Error(`钉钉 API 错误: ${error.response.data?.errmsg || error.message}`);
288
+ }
289
+ throw error;
290
+ }
291
+ }
292
+ }
293
+
294
+ const dingtalk = new DingTalkClient();
295
+
296
+ // MCP Server 定义
297
+ const server = new Server(
298
+ {
299
+ name: 'dingtalk-wiki-mcp',
300
+ version: '1.1.0'
301
+ },
302
+ {
303
+ capabilities: {
304
+ tools: {}
305
+ }
306
+ }
307
+ );
308
+
309
+ // 工具定义
310
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
311
+ return {
312
+ tools: [
313
+ {
314
+ name: 'set_operator',
315
+ description: '设置操作者 unionid(用于访问 Wiki API)',
316
+ inputSchema: {
317
+ type: 'object',
318
+ properties: {
319
+ unionid: {
320
+ type: 'string',
321
+ description: '用户的 unionid'
322
+ }
323
+ },
324
+ required: ['unionid']
325
+ }
326
+ },
327
+ {
328
+ name: 'show_config',
329
+ description: '显示当前配置信息(默认用户和知识库列表)',
330
+ inputSchema: {
331
+ type: 'object',
332
+ properties: {}
333
+ }
334
+ },
335
+ {
336
+ name: 'list_wiki_workspaces',
337
+ description: '列出用户有权限的知识库工作空间列表',
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: {
341
+ operator_id: {
342
+ type: 'string',
343
+ description: '操作者 unionid(不传则使用之前设置的)'
344
+ }
345
+ }
346
+ }
347
+ },
348
+ {
349
+ name: 'get_wiki_workspace',
350
+ description: '获取知识库工作空间详情',
351
+ inputSchema: {
352
+ type: 'object',
353
+ properties: {
354
+ workspace_id: {
355
+ type: 'string',
356
+ description: '知识库工作空间 ID'
357
+ }
358
+ },
359
+ required: ['workspace_id']
360
+ }
361
+ },
362
+ {
363
+ name: 'list_wiki_nodes',
364
+ description: '列出知识库中的节点(文档和目录)',
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ workspace_id: {
369
+ type: 'string',
370
+ description: '知识库工作空间 ID'
371
+ },
372
+ parent_node_id: {
373
+ type: 'string',
374
+ description: '父节点 ID(不传则获取根目录)'
375
+ }
376
+ },
377
+ required: ['workspace_id']
378
+ }
379
+ },
380
+ {
381
+ name: 'create_wiki_doc',
382
+ description: '在知识库中创建新文档(需要 Document.WorkspaceDocument.Write 权限)',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ workspace_id: {
387
+ type: 'string',
388
+ description: '知识库工作空间 ID'
389
+ },
390
+ name: {
391
+ type: 'string',
392
+ description: '文档名称'
393
+ },
394
+ doc_type: {
395
+ type: 'string',
396
+ description: '文档类型: DOC(文字), WORKBOOK(表格), MIND(脑图), FOLDER(文件夹)',
397
+ enum: ['DOC', 'WORKBOOK', 'MIND', 'FOLDER'],
398
+ default: 'DOC'
399
+ },
400
+ parent_node_id: {
401
+ type: 'string',
402
+ description: '父节点 ID(可选,不传则创建在根目录)'
403
+ },
404
+ content: {
405
+ type: 'string',
406
+ description: '文档内容(可选)'
407
+ }
408
+ },
409
+ required: ['workspace_id', 'name']
410
+ }
411
+ },
412
+ {
413
+ name: 'get_wiki_node',
414
+ description: '获取知识库节点详情',
415
+ inputSchema: {
416
+ type: 'object',
417
+ properties: {
418
+ node_id: {
419
+ type: 'string',
420
+ description: '节点 ID'
421
+ }
422
+ },
423
+ required: ['node_id']
424
+ }
425
+ },
426
+ {
427
+ name: 'search_wiki',
428
+ description: '搜索知识库内容',
429
+ inputSchema: {
430
+ type: 'object',
431
+ properties: {
432
+ keyword: {
433
+ type: 'string',
434
+ description: '搜索关键词'
435
+ },
436
+ workspace_id: {
437
+ type: 'string',
438
+ description: '指定知识库 ID(可选)'
439
+ }
440
+ },
441
+ required: ['keyword']
442
+ }
443
+ },
444
+ {
445
+ name: 'list_departments',
446
+ description: '列出钉钉组织架构中的部门列表',
447
+ inputSchema: {
448
+ type: 'object',
449
+ properties: {
450
+ dept_id: {
451
+ type: 'number',
452
+ description: '父部门 ID(默认 1 即根部门,可按需指定)',
453
+ default: 1
454
+ },
455
+ fetch_child: {
456
+ type: 'boolean',
457
+ description: '是否递归获取子部门',
458
+ default: true
459
+ }
460
+ }
461
+ }
462
+ },
463
+ {
464
+ name: 'get_department_users',
465
+ description: '获取部门成员列表',
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ dept_id: {
470
+ type: 'number',
471
+ description: '部门 ID'
472
+ },
473
+ cursor: {
474
+ type: 'number',
475
+ description: '分页游标',
476
+ default: 0
477
+ },
478
+ size: {
479
+ type: 'number',
480
+ description: '每页数量',
481
+ default: 50
482
+ }
483
+ },
484
+ required: ['dept_id']
485
+ }
486
+ },
487
+ {
488
+ name: 'get_user_info',
489
+ description: '获取用户详细信息',
490
+ inputSchema: {
491
+ type: 'object',
492
+ properties: {
493
+ userid: {
494
+ type: 'string',
495
+ description: '用户 ID'
496
+ }
497
+ },
498
+ required: ['userid']
499
+ }
500
+ },
501
+ {
502
+ name: 'list_notable_sheets',
503
+ description: '读取 AI 表格 / Notable 的所有数据表。对于 .able 节点,直接使用 nodeId 作为 base_id。',
504
+ inputSchema: {
505
+ type: 'object',
506
+ properties: {
507
+ base_id: {
508
+ type: 'string',
509
+ description: 'Notable baseId。对 .able 节点来说,通常就是 nodeId。'
510
+ },
511
+ operator_id: {
512
+ type: 'string',
513
+ description: '操作者 unionid(不传则使用默认用户)'
514
+ }
515
+ },
516
+ required: ['base_id']
517
+ }
518
+ },
519
+ {
520
+ name: 'list_notable_records',
521
+ description: '读取 AI 表格 / Notable 某个数据表中的记录。',
522
+ inputSchema: {
523
+ type: 'object',
524
+ properties: {
525
+ base_id: {
526
+ type: 'string',
527
+ description: 'Notable baseId。对 .able 节点来说,通常就是 nodeId。'
528
+ },
529
+ sheet_id: {
530
+ type: 'string',
531
+ description: '数据表 ID,可先通过 list_notable_sheets 获取。'
532
+ },
533
+ max_results: {
534
+ type: 'number',
535
+ description: '返回记录数,默认 20',
536
+ default: 20
537
+ },
538
+ next_token: {
539
+ type: 'string',
540
+ description: '分页 token,可选'
541
+ },
542
+ operator_id: {
543
+ type: 'string',
544
+ description: '操作者 unionid(不传则使用默认用户)'
545
+ }
546
+ },
547
+ required: ['base_id', 'sheet_id']
548
+ }
549
+ }
550
+ ]
551
+ };
552
+ });
553
+
554
+ // 工具调用处理
555
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
556
+ const { name, arguments: args } = request.params;
557
+
558
+ try {
559
+ switch (name) {
560
+ case 'set_operator': {
561
+ const { unionid } = args;
562
+ dingtalk.setOperatorId(unionid);
563
+ return {
564
+ content: [{
565
+ type: 'text',
566
+ text: `✅ 操作者已设置为: ${unionid}`
567
+ }]
568
+ };
569
+ }
570
+
571
+ case 'show_config': {
572
+ let output = '⚙️ 当前配置信息\n\n';
573
+
574
+ output += '👤 默认用户:\n';
575
+ if (userConfig.defaultUser && userConfig.users) {
576
+ const user = userConfig.users[userConfig.defaultUser];
577
+ output += ` 姓名: ${user.name}\n`;
578
+ output += ` User ID: ${user.userId}\n`;
579
+ output += ` Union ID: ${user.unionId}\n`;
580
+ } else {
581
+ output += ' (未配置)\n';
582
+ }
583
+
584
+ output += '\n📚 知识库列表:\n';
585
+ if (userConfig.workspaces) {
586
+ Object.entries(userConfig.workspaces).forEach(([name, info], index) => {
587
+ output += ` ${index + 1}. ${name}\n`;
588
+ output += ` ID: ${info.id}\n`;
589
+ });
590
+ }
591
+
592
+ output += '\n💡 使用提示:\n';
593
+ output += ' - list_wiki_workspaces 和 list_wiki_nodes 会自动使用默认 operator_id\n';
594
+ output += ' - 如需使用其他用户,可传入 operator_id 参数覆盖\n';
595
+
596
+ return {
597
+ content: [{
598
+ type: 'text',
599
+ text: output
600
+ }]
601
+ };
602
+ }
603
+
604
+ case 'list_wiki_workspaces': {
605
+ if (args.operator_id) {
606
+ dingtalk.setOperatorId(args.operator_id);
607
+ }
608
+ const result = await dingtalk.wikiRequest('workspaces');
609
+ const workspaces = result.workspaces || [];
610
+
611
+ let output = `📚 知识库工作空间列表 (${workspaces.length}个)\n\n`;
612
+ workspaces.forEach((ws, index) => {
613
+ output += `${index + 1}. ${ws.name}\n`;
614
+ output += ` ID: ${ws.workspaceId}\n`;
615
+ output += ` 类型: ${ws.type}\n`;
616
+ output += ` 描述: ${ws.description || '无'}\n`;
617
+ output += ` 链接: ${ws.url}\n\n`;
618
+ });
619
+
620
+ return {
621
+ content: [{
622
+ type: 'text',
623
+ text: output
624
+ }]
625
+ };
626
+ }
627
+
628
+ case 'get_wiki_workspace': {
629
+ const { workspace_id } = args;
630
+ // 通过列表获取详情
631
+ const result = await dingtalk.wikiRequest('workspaces');
632
+ const workspaces = result.workspaces || [];
633
+ const workspace = workspaces.find(ws => ws.workspaceId === workspace_id);
634
+
635
+ if (!workspace) {
636
+ return {
637
+ content: [{
638
+ type: 'text',
639
+ text: `⚠️ 未找到知识库: ${workspace_id}`
640
+ }],
641
+ isError: true
642
+ };
643
+ }
644
+
645
+ return {
646
+ content: [{
647
+ type: 'text',
648
+ text: `📚 知识库详情\n\n${JSON.stringify(workspace, null, 2)}`
649
+ }]
650
+ };
651
+ }
652
+
653
+ case 'list_wiki_nodes': {
654
+ const { workspace_id, parent_node_id, operator_id } = args;
655
+ if (operator_id) {
656
+ dingtalk.setOperatorId(operator_id);
657
+ }
658
+
659
+ const params = { workspaceId: workspace_id };
660
+ if (parent_node_id) {
661
+ params.parentNodeId = parent_node_id;
662
+ } else {
663
+ // 如果没有传入 parent_node_id,先获取 workspace 详情得到 rootNodeId
664
+ const workspacesResult = await dingtalk.wikiRequest('workspaces');
665
+ const workspace = workspacesResult.workspaces?.find(ws => ws.workspaceId === workspace_id);
666
+ if (workspace && workspace.rootNodeId) {
667
+ params.parentNodeId = workspace.rootNodeId;
668
+ }
669
+ }
670
+
671
+ const result = await dingtalk.wikiRequest('nodes', params);
672
+ const nodes = result.nodes || [];
673
+
674
+ let output = `📄 知识库节点列表 (${nodes.length}个)\n\n`;
675
+ nodes.forEach((node, index) => {
676
+ const icon = node.type === 'FOLDER' ? '📁' : '📄';
677
+ output += `${index + 1}. ${icon} ${node.name}\n`;
678
+ output += ` ID: ${node.nodeId}\n`;
679
+ output += ` 类型: ${node.type}\n`;
680
+ output += ` 有子节点: ${node.hasChildren ? '是' : '否'}\n`;
681
+ output += ` 链接: ${node.url}\n\n`;
682
+ });
683
+
684
+ return {
685
+ content: [{
686
+ type: 'text',
687
+ text: output
688
+ }]
689
+ };
690
+ }
691
+
692
+ case 'create_wiki_doc': {
693
+ const { workspace_id, parent_node_id, name, doc_type = 'DOC', operator_id } = args;
694
+ if (operator_id) {
695
+ dingtalk.setOperatorId(operator_id);
696
+ }
697
+
698
+ try {
699
+ const opId = await dingtalk.getCurrentUserUnionId();
700
+ if (!opId) {
701
+ throw new Error('未设置 operator_id,请传入 operator_id 或在配置文件中设置默认用户');
702
+ }
703
+
704
+ // 获取 access token
705
+ const token = await dingtalk.getAccessToken();
706
+
707
+ // 构建请求体 - 使用正确的 Document API v1.0
708
+ const requestBody = {
709
+ name: name,
710
+ docType: doc_type,
711
+ operatorId: opId
712
+ };
713
+
714
+ if (parent_node_id) {
715
+ requestBody.parentNodeId = parent_node_id;
716
+ }
717
+
718
+ // 使用正确的 API 端点: POST /v1.0/doc/workspaces/{workspaceId}/docs
719
+ const response = await axios({
720
+ method: 'POST',
721
+ url: `${DINGTALK_API_V2}/v1.0/doc/workspaces/${workspace_id}/docs`,
722
+ headers: {
723
+ 'x-acs-dingtalk-access-token': token,
724
+ 'Content-Type': 'application/json'
725
+ },
726
+ data: requestBody
727
+ });
728
+
729
+ const doc = response.data;
730
+ const typeLabels = {
731
+ DOC: '文档',
732
+ WORKBOOK: '表格',
733
+ MIND: '脑图',
734
+ FOLDER: '文件夹'
735
+ };
736
+ const typeIcons = {
737
+ DOC: '📄',
738
+ WORKBOOK: '📊',
739
+ MIND: '🧠',
740
+ FOLDER: '📁'
741
+ };
742
+ const typeLabel = typeLabels[doc_type] || '文档';
743
+ const typeIcon = typeIcons[doc_type] || '📄';
744
+ const lines = [
745
+ `✅ ${typeLabel}创建成功!`,
746
+ '',
747
+ `${typeIcon} ${name}`,
748
+ `🗂️ 类型: ${doc_type}`,
749
+ `🆔 Node ID: ${doc.nodeId}`
750
+ ];
751
+
752
+ if (doc.docKey) {
753
+ lines.push(`🔑 DocKey: ${doc.docKey}`);
754
+ }
755
+ if (doc.url) {
756
+ lines.push(`🔗 链接: ${doc.url}`);
757
+ }
758
+ if (doc.workspaceId) {
759
+ lines.push(`📂 Workspace ID: ${doc.workspaceId}`);
760
+ }
761
+
762
+ return {
763
+ content: [{
764
+ type: 'text',
765
+ text: lines.join('\n')
766
+ }]
767
+ };
768
+ } catch (error) {
769
+ if (error.response?.data?.code === 'InvalidAuthentication') {
770
+ return {
771
+ content: [{
772
+ type: 'text',
773
+ text: `⚠️ Access Token 已过期或无效\n\n请稍后重试,或检查 AppKey/AppSecret 配置。`
774
+ }],
775
+ isError: true
776
+ };
777
+ }
778
+ if (error.response?.data?.code === 'invalidRequest.workspaceNode.nameConflict') {
779
+ return {
780
+ content: [{
781
+ type: 'text',
782
+ text: `⚠️ 文档名称冲突\n\n知识库中已存在同名文档,请使用其他名称。`
783
+ }],
784
+ isError: true
785
+ };
786
+ }
787
+ if (error.response?.data?.code?.includes('forbidden.accessDenied')) {
788
+ return {
789
+ content: [{
790
+ type: 'text',
791
+ text: `⚠️ 权限不足\n\n错误信息: ${error.response.data.message || error.message}\n\n需要申请的权限: Document.WorkspaceDocument.Write - 创建文档权限`
792
+ }],
793
+ isError: true
794
+ };
795
+ }
796
+ throw error;
797
+ }
798
+ }
799
+
800
+ case 'get_wiki_node': {
801
+ const { node_id } = args;
802
+ // 通过搜索或其他方式获取节点详情
803
+ return {
804
+ content: [{
805
+ type: 'text',
806
+ text: `📄 节点 ID: ${node_id}\n\n请使用 list_wiki_nodes 获取节点列表,然后通过节点链接访问详情。`
807
+ }]
808
+ };
809
+ }
810
+
811
+ case 'search_wiki': {
812
+ const { keyword, workspace_id } = args;
813
+ // Wiki 搜索 API 需要额外权限
814
+ return {
815
+ content: [{
816
+ type: 'text',
817
+ text: `🔍 搜索知识库: ${keyword}\n\n搜索功能需要 Wiki.Search 权限。\n\n请直接访问知识库网页版进行搜索:\nhttps://alidocs.dingtalk.com/i/spaces/${workspace_id || ''}/search?keyword=${encodeURIComponent(keyword)}`
818
+ }]
819
+ };
820
+ }
821
+
822
+ case 'list_departments': {
823
+ const result = await dingtalk.oapiRequest('v2/department/listsub', {
824
+ dept_id: args.dept_id || 1,
825
+ fetch_child: args.fetch_child !== false
826
+ });
827
+
828
+ return {
829
+ content: [{
830
+ type: 'text',
831
+ text: `✅ 部门列表 (${result.result?.length || 0}个)\n\n${JSON.stringify(result.result || [], null, 2)}`
832
+ }]
833
+ };
834
+ }
835
+
836
+ case 'get_department_users': {
837
+ const { dept_id, cursor = 0, size = 50 } = args;
838
+ const result = await dingtalk.oapiRequest('v2/user/list', {
839
+ dept_id,
840
+ cursor,
841
+ size
842
+ });
843
+
844
+ return {
845
+ content: [{
846
+ type: 'text',
847
+ text: `✅ 部门成员列表\n\n${JSON.stringify(result.result || {}, null, 2)}`
848
+ }]
849
+ };
850
+ }
851
+
852
+ case 'get_user_info': {
853
+ const { userid } = args;
854
+ const result = await dingtalk.oapiRequest('v2/user/get', {
855
+ userid
856
+ });
857
+
858
+ const userInfo = result.result || {};
859
+ let output = `✅ 用户信息\n\n${JSON.stringify(userInfo, null, 2)}\n\n`;
860
+ if (userInfo.unionid && userInfo.userid) {
861
+ output += `💡 提示: 将以下 userId 写入 config.json 的 defaultUser,程序会自动获取并缓存 unionId:\n "${userInfo.userid}"`;
862
+ }
863
+
864
+ return {
865
+ content: [{
866
+ type: 'text',
867
+ text: output
868
+ }]
869
+ };
870
+ }
871
+
872
+ case 'list_notable_sheets': {
873
+ const { base_id, operator_id } = args;
874
+ const result = await dingtalk.notableRequest('GET', `/v1.0/notable/bases/${base_id}/sheets`, {
875
+ operatorId: operator_id || null
876
+ });
877
+ const sheets = result.value || [];
878
+ let output = `📊 数据表列表 (${sheets.length}个)\n\n`;
879
+ sheets.forEach((sheet, index) => {
880
+ output += `${index + 1}. ${sheet.name}\n`;
881
+ output += ` ID: ${sheet.id}\n\n`;
882
+ });
883
+ if (!sheets.length) {
884
+ output += '(没有返回任何数据表)';
885
+ }
886
+ output += `💡 说明: 对 .able 节点,通常直接使用 nodeId 作为 base_id。`;
887
+
888
+ return {
889
+ content: [{
890
+ type: 'text',
891
+ text: output
892
+ }]
893
+ };
894
+ }
895
+
896
+ case 'list_notable_records': {
897
+ const { base_id, sheet_id, max_results = 20, next_token, operator_id } = args;
898
+ const payload = {
899
+ maxResults: max_results
900
+ };
901
+ if (next_token) {
902
+ payload.nextToken = next_token;
903
+ }
904
+ const result = await dingtalk.notableRequest('POST', `/v1.0/notable/bases/${base_id}/sheets/${sheet_id}/records/list`, {
905
+ operatorId: operator_id || null,
906
+ data: payload
907
+ });
908
+ const records = result.records || [];
909
+ let output = `📋 数据表记录 (${records.length}条)\n\n`;
910
+ output += `${JSON.stringify(records, null, 2)}\n\n`;
911
+ output += `hasMore: ${result.hasMore ? 'true' : 'false'}\n`;
912
+ output += `nextToken: ${result.nextToken || ''}`;
913
+
914
+ return {
915
+ content: [{
916
+ type: 'text',
917
+ text: output
918
+ }]
919
+ };
920
+ }
921
+
922
+ default:
923
+ throw new Error(`未知工具: ${name}`);
924
+ }
925
+ } catch (error) {
926
+ return {
927
+ content: [{
928
+ type: 'text',
929
+ text: `❌ 错误: ${error.message}`
930
+ }],
931
+ isError: true
932
+ };
933
+ }
934
+ });
935
+
936
+ // 启动服务器
937
+ async function main() {
938
+ const transport = new StdioServerTransport();
939
+ await server.connect(transport);
940
+ console.error('钉钉 Wiki MCP Server 已启动 v2.0');
941
+ console.error(`Config path: ${CONFIG_PATH}`);
942
+ }
943
+
944
+ main().catch(console.error);