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/LICENSE +21 -0
- package/README.md +339 -0
- package/README.zh-CN.md +331 -0
- package/docs/clients/generic-mcp-client.md +20 -0
- package/docs/clients/mcporter.md +32 -0
- package/docs/clients/openclaw.md +31 -0
- package/docs/skill-reference.md +108 -0
- package/index.js +944 -0
- package/package.json +45 -0
- package/skill/SKILL.md +56 -0
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);
|