dingtalk-wiki 1.1.5 → 1.2.0

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.
Files changed (3) hide show
  1. package/index.js +404 -34
  2. package/package.json +3 -1
  3. package/skill/SKILL.md +18 -4
package/index.js CHANGED
@@ -11,6 +11,8 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
11
11
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
12
12
  const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
13
13
  const axios = require('axios');
14
+ const MiniSearch = require('minisearch');
15
+ const Database = require('better-sqlite3');
14
16
  const dotenv = require('dotenv');
15
17
 
16
18
  // 钉钉 API 配置
@@ -19,8 +21,9 @@ const DINGTALK_API_V2 = 'https://api.dingtalk.com';
19
21
  const fs = require('fs');
20
22
  const path = require('path');
21
23
  const os = require('os');
22
- const CACHE_DIR = path.join(os.homedir(), '.cache', 'dingtalk-wiki-mcp');
24
+ const CACHE_DIR = path.join(os.homedir(), '.cache', 'dingtalk-wiki');
23
25
  const UNIONID_CACHE_PATH = path.join(CACHE_DIR, 'unionid-cache.json');
26
+ const SEARCH_INDEX_PATH = path.join(CACHE_DIR, 'search-index.sqlite');
24
27
 
25
28
  if (!fs.existsSync(CACHE_DIR)) {
26
29
  fs.mkdirSync(CACHE_DIR, { recursive: true });
@@ -43,6 +46,221 @@ function saveUnionIdCache() {
43
46
  }
44
47
  }
45
48
 
49
+ class WikiSearchIndex {
50
+ constructor() {
51
+ this.db = new Database(SEARCH_INDEX_PATH);
52
+ this.db.pragma('journal_mode = WAL');
53
+ this.db.exec(`CREATE TABLE IF NOT EXISTS docs (
54
+ id TEXT PRIMARY KEY,
55
+ title TEXT NOT NULL DEFAULT '',
56
+ content TEXT NOT NULL DEFAULT '',
57
+ workspaceId TEXT NOT NULL DEFAULT '',
58
+ workspaceName TEXT NOT NULL DEFAULT '',
59
+ url TEXT NOT NULL DEFAULT '',
60
+ type TEXT NOT NULL DEFAULT ''
61
+ )`);
62
+ this._insertStmt = this.db.prepare(`INSERT OR REPLACE INTO docs (id, title, content, workspaceId, workspaceName, url, type) VALUES (?, ?, ?, ?, ?, ?, ?)`);
63
+ this._deleteStmt = this.db.prepare(`DELETE FROM docs WHERE id = ?`);
64
+ this._getStmt = this.db.prepare(`SELECT * FROM docs WHERE id = ?`);
65
+
66
+ this.index = new MiniSearch({
67
+ fields: ['title', 'content'],
68
+ storeFields: ['title', 'workspaceId', 'workspaceName', 'url', 'type'],
69
+ });
70
+
71
+ this._load();
72
+ }
73
+
74
+ _load() {
75
+ try {
76
+ const rows = this.db.prepare('SELECT id, title, content, workspaceId, workspaceName, url, type FROM docs').all();
77
+ for (const row of rows) {
78
+ this.index.add({
79
+ id: row.id, title: row.title, content: row.content,
80
+ workspaceId: row.workspaceId, workspaceName: row.workspaceName,
81
+ url: row.url, type: row.type,
82
+ });
83
+ }
84
+ console.error(`[钉钉MCP] 搜索索引已加载 (${rows.length} 篇文档)`);
85
+ } catch (e) {
86
+ console.error('[钉钉MCP] 加载搜索索引失败:', e.message);
87
+ }
88
+ }
89
+
90
+ add(docKey, data) {
91
+ this._insertStmt.run(docKey, data.title || '', data.content || '', data.workspaceId || '', data.workspaceName || '', data.url || '', data.type || '');
92
+ try {
93
+ this.index.add({ id: docKey, ...data });
94
+ } catch (e) {
95
+ console.error(`[钉钉MCP] 索引添加失败 [${docKey}]:`, e.message);
96
+ }
97
+ }
98
+
99
+ remove(docKey) {
100
+ this._deleteStmt.run(docKey);
101
+ try { this.index.remove({ id: docKey }); } catch (e) { /* not in index */ }
102
+ }
103
+
104
+ update(docKey, data) {
105
+ const existing = this._getStmt.get(docKey);
106
+ const merged = {
107
+ title: data.title ?? existing?.title ?? '',
108
+ content: data.content ?? existing?.content ?? '',
109
+ workspaceId: data.workspaceId ?? existing?.workspaceId ?? '',
110
+ workspaceName: data.workspaceName ?? existing?.workspaceName ?? '',
111
+ url: data.url ?? existing?.url ?? '',
112
+ type: data.type ?? existing?.type ?? '',
113
+ };
114
+ this._insertStmt.run(docKey, merged.title, merged.content, merged.workspaceId, merged.workspaceName, merged.url, merged.type);
115
+ try { this.index.remove({ id: docKey }); } catch (e) { /* ignore */ }
116
+ try {
117
+ this.index.add({ id: docKey, ...merged });
118
+ } catch (e) {
119
+ console.error(`[钉钉MCP] 索引更新失败 [${docKey}]:`, e.message);
120
+ }
121
+ }
122
+
123
+ search(keyword, maxResults = 20) {
124
+ const results = this.index.search(keyword, { prefix: true, fuzzy: 0.2 });
125
+ const top = results.slice(0, maxResults);
126
+ if (top.length === 0) return [];
127
+
128
+ const ids = top.map(r => r.id);
129
+ const placeholders = ids.map(() => '?').join(',');
130
+ const rows = this.db.prepare(`SELECT * FROM docs WHERE id IN (${placeholders})`).all(...ids);
131
+ const rowMap = Object.fromEntries(rows.map(r => [r.id, r]));
132
+
133
+ return top.map(r => ({
134
+ id: r.id, score: r.score,
135
+ title: rowMap[r.id]?.title || '',
136
+ content: rowMap[r.id]?.content || '',
137
+ workspaceId: rowMap[r.id]?.workspaceId || '',
138
+ workspaceName: rowMap[r.id]?.workspaceName || '',
139
+ url: rowMap[r.id]?.url || '',
140
+ type: rowMap[r.id]?.type || '',
141
+ }));
142
+ }
143
+
144
+ get size() {
145
+ return this.db.prepare('SELECT COUNT(*) as c FROM docs').get().c;
146
+ }
147
+
148
+ clear() {
149
+ this.db.exec('DELETE FROM docs');
150
+ this.index = new MiniSearch({
151
+ fields: ['title', 'content'],
152
+ storeFields: ['title', 'workspaceId', 'workspaceName', 'url', 'type'],
153
+ });
154
+ }
155
+ }
156
+
157
+ async function reindexNotableBase(baseId) {
158
+ try {
159
+ const sheetsRes = await dingtalk.notableRequest('GET', `/v1.0/notable/bases/${baseId}/sheets`);
160
+ const sheets = sheetsRes.value || [];
161
+ const texts = [];
162
+ for (const sheet of sheets) {
163
+ try {
164
+ const recordsRes = await dingtalk.notableRequest('POST', `/v1.0/notable/bases/${baseId}/sheets/${sheet.id}/records/list`, {
165
+ data: { maxResults: 500 }
166
+ });
167
+ const records = recordsRes.records || [];
168
+ for (const rec of records) {
169
+ for (const val of Object.values(rec.fields || {})) {
170
+ if (typeof val === 'string' || typeof val === 'number') {
171
+ texts.push(String(val));
172
+ }
173
+ }
174
+ }
175
+ } catch (e) { /* skip sheet */ }
176
+ }
177
+ wikiIndex.update(baseId, { content: texts.join('\n') });
178
+ } catch (e) {
179
+ console.error(`[钉钉MCP] 重建 Notable 表索引失败 [${baseId}]:`, e.message);
180
+ }
181
+ }
182
+
183
+ async function rebuildSearchIndex() {
184
+ const wsResult = await dingtalk.wikiRequest('workspaces');
185
+ const workspaces = wsResult.workspaces || [];
186
+
187
+ wikiIndex.clear();
188
+ let total = 0;
189
+
190
+ for (const ws of workspaces) {
191
+ const queue = [ws.rootNodeId];
192
+ const visited = new Set();
193
+
194
+ while (queue.length > 0) {
195
+ const dentryId = queue.shift();
196
+ if (!dentryId || visited.has(dentryId)) continue;
197
+ visited.add(dentryId);
198
+
199
+ try {
200
+ const result = await dingtalk.docRequest('GET', `/v2.0/doc/spaces/${ws.workspaceId}/directories`, {
201
+ extraParams: dentryId !== ws.rootNodeId ? { dentryId, maxResults: 500 } : { maxResults: 500 }
202
+ });
203
+
204
+ const children = result.children || [];
205
+ for (const child of children) {
206
+ const childId = child.dentryId || child.nodeId || child.id;
207
+ const name = child.name || '';
208
+ const isFolder = child.contentType === 'folder';
209
+
210
+ if (!isFolder && childId) {
211
+ let content = '';
212
+ try {
213
+ const blocks = await dingtalk.docRequest('GET', `/v1.0/doc/suites/documents/${childId}/blocks`);
214
+ const blockData = blocks.result?.data || [];
215
+ content = blockData.map(b => extractBlockText(b)).join('\n\n');
216
+ } catch (e) { /* content unavailable */ }
217
+
218
+ if (!content) {
219
+ try {
220
+ const sheetsRes = await dingtalk.notableRequest('GET', `/v1.0/notable/bases/${childId}/sheets`);
221
+ const sheets = sheetsRes.value || [];
222
+ const texts = [];
223
+ for (const sheet of sheets) {
224
+ try {
225
+ const recordsRes = await dingtalk.notableRequest('POST', `/v1.0/notable/bases/${childId}/sheets/${sheet.id}/records/list`, {
226
+ data: { maxResults: 500 }
227
+ });
228
+ const records = recordsRes.records || [];
229
+ for (const rec of records) {
230
+ for (const val of Object.values(rec.fields || {})) {
231
+ if (typeof val === 'string' || typeof val === 'number') {
232
+ texts.push(String(val));
233
+ }
234
+ }
235
+ }
236
+ } catch (e) { /* skip sheet */ }
237
+ }
238
+ content = texts.join('\n');
239
+ } catch (e) { /* not a Notable table */ }
240
+ }
241
+
242
+ wikiIndex.add(childId, {
243
+ title: name, content,
244
+ workspaceId: ws.workspaceId,
245
+ workspaceName: ws.name,
246
+ url: child.url || '',
247
+ type: child.contentType || 'DOC',
248
+ });
249
+ total++;
250
+ }
251
+
252
+ if (child.hasChildren && childId && !visited.has(childId)) {
253
+ queue.push(childId);
254
+ }
255
+ }
256
+ } catch (e) { /* skip errored nodes */ }
257
+ }
258
+ }
259
+
260
+ console.error(`[钉钉MCP] 搜索索引重建完成 (${total} 篇文档)`);
261
+ return total;
262
+ }
263
+
46
264
  function loadEnvFile(filePath) {
47
265
  if (!filePath || !fs.existsSync(filePath)) {
48
266
  return false;
@@ -286,7 +504,7 @@ class DingTalkClient {
286
504
  return this.operatorId;
287
505
  }
288
506
 
289
- async docRequest(method, pathName, { operatorId = null, data = null } = {}) {
507
+ async docRequest(method, pathName, { operatorId = null, data = null, extraParams = {} } = {}) {
290
508
  const token = await this.getAccessToken();
291
509
  const resolvedOperatorId = await this.resolveOperatorId(operatorId);
292
510
  const url = `${DINGTALK_API_V2}${pathName}`;
@@ -300,7 +518,8 @@ class DingTalkClient {
300
518
  'Content-Type': 'application/json'
301
519
  },
302
520
  params: {
303
- operatorId: resolvedOperatorId
521
+ operatorId: resolvedOperatorId,
522
+ ...extraParams
304
523
  },
305
524
  data
306
525
  });
@@ -377,6 +596,7 @@ class DingTalkClient {
377
596
  }
378
597
 
379
598
  const dingtalk = new DingTalkClient();
599
+ const wikiIndex = new WikiSearchIndex();
380
600
 
381
601
  // MCP Server 定义
382
602
  const server = new Server(
@@ -502,7 +722,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
502
722
  },
503
723
  {
504
724
  name: 'search_wiki',
505
- description: '搜索知识库(POST /v2.0/doc/search)',
725
+ description: '按名称搜索知识库中的文档和文件夹(遍历目录树,无需索引即可使用)',
506
726
  inputSchema: {
507
727
  type: 'object',
508
728
  properties: {
@@ -512,15 +732,50 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
512
732
  },
513
733
  workspace_id: {
514
734
  type: 'string',
515
- description: '指定知识库 ID(可选)'
735
+ description: '指定知识库 ID(可选,不传则搜索所有知识库)'
516
736
  },
517
737
  max_results: {
518
738
  type: 'number',
519
- description: '返回条数上限(默认 10,最大 20)'
739
+ description: '返回条数上限(默认 20,最大 50)'
520
740
  },
521
- next_token: {
741
+ operator_id: {
742
+ type: 'string',
743
+ description: '操作者 unionid(不传则使用默认用户)'
744
+ }
745
+ },
746
+ required: ['keyword']
747
+ }
748
+ },
749
+ {
750
+ name: 'refresh_search_index',
751
+ description: '全量重建搜索索引(遍历所有知识库读取文档内容,建立全文索引后 search_wiki_content 方可使用)',
752
+ inputSchema: {
753
+ type: 'object',
754
+ properties: {
755
+ operator_id: {
756
+ type: 'string',
757
+ description: '操作者 unionid(不传则使用默认用户)'
758
+ }
759
+ }
760
+ }
761
+ },
762
+ {
763
+ name: 'search_wiki_content',
764
+ description: '全文搜索知识库文档内容(需先运行 refresh_search_index 建立索引)',
765
+ inputSchema: {
766
+ type: 'object',
767
+ properties: {
768
+ keyword: {
522
769
  type: 'string',
523
- description: '分页游标(上次返回的 nextToken)'
770
+ description: '搜索关键词'
771
+ },
772
+ workspace_id: {
773
+ type: 'string',
774
+ description: '指定知识库 ID(可选,不传则搜索所有知识库)'
775
+ },
776
+ max_results: {
777
+ type: 'number',
778
+ description: '返回条数上限(默认 20,最大 50)'
524
779
  },
525
780
  operator_id: {
526
781
  type: 'string',
@@ -1055,6 +1310,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1055
1310
  lines.push(`📂 Workspace ID: ${doc.workspaceId}`);
1056
1311
  }
1057
1312
 
1313
+ setImmediate(() => {
1314
+ wikiIndex.add(doc.nodeId || doc.docKey, {
1315
+ title: name,
1316
+ content: args.content || '',
1317
+ workspaceId: workspace_id || doc.workspaceId,
1318
+ workspaceName: '',
1319
+ url: doc.url || '',
1320
+ type: doc_type,
1321
+ });
1322
+ if (args.content && doc.nodeId) {
1323
+ dingtalk.docRequest('POST', `/v1.0/doc/suites/documents/${doc.nodeId}/overwriteContent`, {
1324
+ data: { content: args.content, contentType: 'markdown' }
1325
+ }).catch(() => {});
1326
+ }
1327
+ });
1328
+
1058
1329
  return {
1059
1330
  content: [{
1060
1331
  type: 'text',
@@ -1120,40 +1391,119 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1120
1391
  }
1121
1392
 
1122
1393
  case 'search_wiki': {
1123
- const { keyword, workspace_id, max_results = 10, next_token, operator_id } = args;
1394
+ const { keyword, workspace_id, max_results = 20, operator_id } = args;
1395
+ if (!keyword) {
1396
+ return {
1397
+ content: [{ type: 'text', text: '⚠️ 请提供搜索关键词 keyword' }],
1398
+ isError: true
1399
+ };
1400
+ }
1401
+
1124
1402
  if (operator_id) {
1125
1403
  dingtalk.setOperatorId(operator_id);
1126
1404
  }
1127
- const body = {
1128
- keyword,
1129
- maxResults: Math.min(max_results, 20)
1130
- };
1131
- if (next_token) {
1132
- body.nextToken = next_token;
1133
- }
1405
+
1406
+ const MAX_RESULTS = Math.min(max_results, 50);
1407
+ const wsResult = await dingtalk.wikiRequest('workspaces');
1408
+ let workspaces = wsResult.workspaces || [];
1134
1409
  if (workspace_id) {
1135
- body.option = { workspaceIds: [workspace_id] };
1410
+ workspaces = workspaces.filter(ws => ws.workspaceId === workspace_id);
1136
1411
  }
1137
- const result = await dingtalk.docRequest('POST', '/v2.0/doc/search', {
1138
- operatorId: operator_id || null,
1139
- data: body
1140
- });
1141
- const items = result.items || [];
1142
- let output = `🔍 搜索 "${keyword}" (${items.length}条)\n\n`;
1143
- items.forEach((item, i) => {
1144
- output += `${i + 1}. ${item.name}\n`;
1145
- output += ` 知识库: ${item.workspaceId}\n`;
1146
- output += ` 链接: ${item.url}\n\n`;
1412
+
1413
+ if (workspaces.length === 0) {
1414
+ return {
1415
+ content: [{ type: 'text', text: '⚠️ 未找到可搜索的知识库' }],
1416
+ isError: true
1417
+ };
1418
+ }
1419
+
1420
+ const matchedNodes = [];
1421
+ for (const ws of workspaces) {
1422
+ if (matchedNodes.length >= MAX_RESULTS) break;
1423
+ const queue = [ws.rootNodeId];
1424
+ const visited = new Set();
1425
+ while (queue.length > 0 && matchedNodes.length < MAX_RESULTS) {
1426
+ const dentryId = queue.shift();
1427
+ if (!dentryId || visited.has(dentryId)) continue;
1428
+ visited.add(dentryId);
1429
+ try {
1430
+ const result = await dingtalk.docRequest('GET', `/v2.0/doc/spaces/${ws.workspaceId}/directories`, {
1431
+ extraParams: dentryId !== ws.rootNodeId ? { dentryId, maxResults: 500 } : { maxResults: 500 }
1432
+ });
1433
+ const children = result.children || [];
1434
+ for (const child of children) {
1435
+ const name = child.name || '';
1436
+ if (name.includes(keyword)) {
1437
+ matchedNodes.push({ name, nodeId: child.dentryId || child.nodeId || child.id, workspaceId: ws.workspaceId, workspaceName: ws.name, type: child.contentType === 'folder' ? '文件夹' : '文档', url: child.url || '' });
1438
+ }
1439
+ const childId = child.dentryId || child.nodeId || child.id;
1440
+ if (child.hasChildren && childId && !visited.has(childId)) queue.push(childId);
1441
+ }
1442
+ } catch (e) { /* skip */ }
1443
+ }
1444
+ }
1445
+
1446
+ let output = `🔍 搜索 "${keyword}" (${matchedNodes.length}条,按名称匹配)\n\n`;
1447
+ matchedNodes.slice(0, MAX_RESULTS).forEach((item, i) => {
1448
+ const icon = item.type === '文件夹' ? '📁' : '📄';
1449
+ output += `${i + 1}. ${icon} ${item.name}\n 知识库: ${item.workspaceName} (${item.workspaceId})\n`;
1450
+ if (item.url) output += ` 链接: ${item.url}\n`;
1451
+ output += '\n';
1147
1452
  });
1148
- if (!items.length) {
1149
- output += '没有找到匹配的知识库。\n';
1453
+ if (matchedNodes.length === 0) {
1454
+ output += '没有找到匹配的文档或文件夹。\n';
1150
1455
  }
1151
- if (result.nextToken) {
1152
- output += `--- 更多结果, nextToken: ${result.nextToken} ---\n`;
1456
+
1457
+ return { content: [{ type: 'text', text: output }] };
1458
+ }
1459
+
1460
+ case 'search_wiki_content': {
1461
+ const { keyword, workspace_id, max_results = 20, operator_id } = args;
1462
+ if (!keyword) {
1463
+ return {
1464
+ content: [{ type: 'text', text: '⚠️ 请提供搜索关键词 keyword' }],
1465
+ isError: true
1466
+ };
1153
1467
  }
1154
- return {
1155
- content: [{ type: 'text', text: output }]
1156
- };
1468
+
1469
+ if (operator_id) {
1470
+ dingtalk.setOperatorId(operator_id);
1471
+ }
1472
+
1473
+ if (wikiIndex.size === 0) {
1474
+ return {
1475
+ content: [{
1476
+ type: 'text',
1477
+ text: '🔍 全文搜索索引为空,请先运行 refresh_search_index 工具遍历知识库建立索引。'
1478
+ }]
1479
+ };
1480
+ }
1481
+
1482
+ const MAX_RESULTS = Math.min(max_results, 50);
1483
+ let results = wikiIndex.search(keyword, MAX_RESULTS);
1484
+ if (workspace_id) {
1485
+ results = results.filter(r => r.workspaceId === workspace_id);
1486
+ }
1487
+
1488
+ let output = `🔍 全文搜索 "${keyword}" (${results.length}条)\n\n`;
1489
+ results.slice(0, MAX_RESULTS).forEach((item, i) => {
1490
+ const icon = item.type === 'FOLDER' || item.type === 'folder' ? '📁' : '📄';
1491
+ output += `${i + 1}. ${icon} ${item.title}\n`;
1492
+ output += ` 知识库: ${item.workspaceName} (${item.workspaceId})\n`;
1493
+ output += ` 匹配度: ${(item.score * 100).toFixed(0)}%\n`;
1494
+ if (item.url) output += ` 链接: ${item.url}\n`;
1495
+ if (item.content) {
1496
+ const preview = item.content.slice(0, 120).replace(/\n+/g, ' ');
1497
+ output += ` 内容预览: ${preview}${item.content.length > 120 ? '...' : ''}\n`;
1498
+ }
1499
+ output += '\n';
1500
+ });
1501
+
1502
+ if (results.length === 0) {
1503
+ output += '没有找到匹配的文档。\n';
1504
+ }
1505
+
1506
+ return { content: [{ type: 'text', text: output }] };
1157
1507
  }
1158
1508
 
1159
1509
  case 'list_departments': {
@@ -1238,6 +1588,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1238
1588
  operatorId: operator_id || null,
1239
1589
  data: { content, contentType: 'markdown' }
1240
1590
  });
1591
+ setImmediate(() => { wikiIndex.update(docKey, { content }); });
1241
1592
  return {
1242
1593
  content: [{ type: 'text', text: '✅ 文档内容已更新' }]
1243
1594
  };
@@ -1252,6 +1603,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1252
1603
  operatorId: operator_id || null,
1253
1604
  data: { name }
1254
1605
  });
1606
+ setImmediate(() => { wikiIndex.update(node_id, { title: name }); });
1255
1607
  return {
1256
1608
  content: [{ type: 'text', text: `✅ 文档已重命名为: ${name}` }]
1257
1609
  };
@@ -1265,6 +1617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1265
1617
  await dingtalk.docRequest('DELETE', `/v1.0/doc/workspaces/${workspace_id}/docs/${node_id}`, {
1266
1618
  operatorId: operator_id || null
1267
1619
  });
1620
+ setImmediate(() => { wikiIndex.remove(node_id); });
1268
1621
  return {
1269
1622
  content: [{
1270
1623
  type: 'text',
@@ -1329,6 +1682,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1329
1682
  operatorId: operator_id || null,
1330
1683
  data: { records }
1331
1684
  });
1685
+ setImmediate(() => reindexNotableBase(base_id));
1332
1686
  return {
1333
1687
  content: [{
1334
1688
  type: 'text',
@@ -1343,6 +1697,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1343
1697
  operatorId: operator_id || null,
1344
1698
  data: { records }
1345
1699
  });
1700
+ setImmediate(() => reindexNotableBase(base_id));
1346
1701
  return {
1347
1702
  content: [{
1348
1703
  type: 'text',
@@ -1357,6 +1712,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1357
1712
  operatorId: operator_id || null,
1358
1713
  data: { recordIds: record_ids }
1359
1714
  });
1715
+ setImmediate(() => reindexNotableBase(base_id));
1360
1716
  return {
1361
1717
  content: [{
1362
1718
  type: 'text',
@@ -1396,6 +1752,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1396
1752
  };
1397
1753
  }
1398
1754
 
1755
+ case 'refresh_search_index': {
1756
+ const { operator_id } = args || {};
1757
+ if (operator_id) {
1758
+ dingtalk.setOperatorId(operator_id);
1759
+ }
1760
+ rebuildSearchIndex().catch(e => console.error('[钉钉MCP] 索引重建失败:', e.message));
1761
+ return {
1762
+ content: [{
1763
+ type: 'text',
1764
+ text: '🔄 搜索索引全量重建已启动(后台运行),完成后搜索将支持全文检索。\n\n这可能需要一些时间,取决于知识库中文档的数量和内容长度。'
1765
+ }]
1766
+ };
1767
+ }
1768
+
1399
1769
  default:
1400
1770
  throw new Error(`未知工具: ${name}`);
1401
1771
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-wiki",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "DingTalk Wiki / Docs read-write MCP server that fills the gap left by DingTalk official MCP.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -40,7 +40,9 @@
40
40
  "dependencies": {
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
42
  "axios": "latest",
43
+ "better-sqlite3": "^12.10.0",
43
44
  "dotenv": "latest",
45
+ "minisearch": "^7.2.0",
44
46
  "zod": "latest"
45
47
  }
46
48
  }
package/skill/SKILL.md CHANGED
@@ -3,6 +3,9 @@
3
3
  钉钉知识库 MCP Server,支持通过 MCP 协议读写钉钉 Wiki / Docs 内容。
4
4
 
5
5
  ### 知识库管理
6
+ - `search_wiki` - 按名称搜索知识库文档和文件夹(遍历目录树,无需索引)
7
+ - `search_wiki_content` - 全文搜索文档内容 + Notable 表格内容(需先 `refresh_search_index`)
8
+ - `refresh_search_index` - 全量重建搜索索引(支持普通文档和 Notable 表格)
6
9
  - `list_wiki_workspaces` - 列出知识库工作空间列表
7
10
  - `get_wiki_workspace` - 获取知识库详情
8
11
  - `list_wiki_nodes` - 列出知识库节点(文档 / 目录)
@@ -12,7 +15,6 @@
12
15
  - `update_wiki_doc_content` - 覆写文档内容(Markdown,⚠️ 全量覆盖)
13
16
  - `rename_wiki_doc` - 重命名文档
14
17
  - `delete_wiki_doc` - 删除文档节点
15
- - `search_wiki` - 搜索知识库内容
16
18
  - `list_notable_sheets` - 获取 `.able` / AI 表格中的所有数据表
17
19
  - `list_notable_records` - 获取指定数据表中的 records
18
20
  - `create_notable_record` - 创建记录(单条或多条)
@@ -100,7 +102,7 @@ mcporter call dingtalk-wiki.delete_wiki_doc \
100
102
  workspace_id="your_workspace_id" \
101
103
  node_id="your_node_id"
102
104
 
103
- # 搜索知识库
105
+ # 按名称搜索
104
106
  mcporter call dingtalk-wiki.search_wiki keyword="项目规划"
105
107
 
106
108
  # 在指定知识库内搜索
@@ -108,11 +110,23 @@ mcporter call dingtalk-wiki.search_wiki keyword="项目规划" workspace_id="you
108
110
 
109
111
  # 自定义返回条数
110
112
  mcporter call dingtalk-wiki.search_wiki keyword="项目规划" max_results=5
113
+ ```
114
+
115
+ ### 全文搜索
116
+
117
+ ```bash
118
+ # 先重建索引(遍历所有知识库获取文档和 Notable 表格内容)
119
+ mcporter call dingtalk-wiki.refresh_search_index
120
+
121
+ # 全文搜索文档内容(包括 Notable 表格记录)
122
+ mcporter call dingtalk-wiki.search_wiki_content keyword="项目规划"
111
123
 
112
- # 分页搜索
113
- mcporter call dingtalk-wiki.search_wiki keyword="项目规划" next_token="your_next_token"
124
+ # 搜索结果含匹配度和内容预览
125
+ mcporter call dingtalk-wiki.search_wiki_content keyword="API集成" max_results=10
114
126
  ```
115
127
 
128
+ > 💡 `search_wiki`(按名称搜索)无需索引即可使用,`search_wiki_content`(全文搜索)需先运行 `refresh_search_index`。
129
+
116
130
  ### AI 表格(Notable)
117
131
 
118
132
  ```bash