dingtalk-wiki 1.1.6 → 1.2.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/README.md +26 -15
- package/README.zh-CN.md +26 -16
- package/index.js +352 -36
- package/package.json +3 -1
- package/skill/SKILL.md +18 -4
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
## Repository Highlights
|
|
16
16
|
|
|
17
17
|
- **Official MCP gap**: Wiki / Docs read-write is not covered
|
|
18
|
-
- **This project adds it**: workspace browsing, node browsing, and
|
|
18
|
+
- **This project adds it**: workspace browsing, node browsing, document CRUD, and full-text search
|
|
19
|
+
- **Local search index**: MiniSearch + SQLite — full-text content search across docs and Notable tables
|
|
19
20
|
- **MCP-compatible**: works with stdio-based MCP clients
|
|
20
21
|
- **Agent-ready**: includes `SKILL.md` for OpenClaw-style skill workflows
|
|
21
22
|
|
|
@@ -124,6 +125,8 @@ node index.js
|
|
|
124
125
|
| Browse workspaces | Not covered | ✅ |
|
|
125
126
|
| Browse nodes / folders | Not covered | ✅ |
|
|
126
127
|
| Read Notable / `.able` records | Not covered | ✅ |
|
|
128
|
+
| Name search | Not covered | ✅ |
|
|
129
|
+
| Full-text content search | Not covered | ✅ MiniSearch + SQLite |
|
|
127
130
|
| MCP client compatibility | Partial / official scope only | ✅ stdio MCP-compatible |
|
|
128
131
|
| OpenClaw skill packaging | No | ✅ includes `SKILL.md` |
|
|
129
132
|
|
|
@@ -143,7 +146,9 @@ node index.js
|
|
|
143
146
|
- `WORKBOOK`
|
|
144
147
|
- `MIND`
|
|
145
148
|
- `FOLDER`
|
|
146
|
-
- Search
|
|
149
|
+
- Search by name (`search_wiki`, BFS directory traversal, always available)
|
|
150
|
+
- Full-text content search (`search_wiki_content`, MiniSearch + SQLite index)
|
|
151
|
+
- Search index management (`refresh_search_index`)
|
|
147
152
|
- Read Notable / `.able` sheets and records via official API
|
|
148
153
|
|
|
149
154
|
### Organization
|
|
@@ -243,17 +248,22 @@ mcporter call --stdio "node ./index.js" create_wiki_doc workspace_id="your_works
|
|
|
243
248
|
|
|
244
249
|
## Available MCP tools
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
- `get_wiki_workspace`
|
|
248
|
-
- `list_wiki_nodes`
|
|
249
|
-
- `
|
|
250
|
-
- `
|
|
251
|
-
- `search_wiki`
|
|
252
|
-
- `
|
|
253
|
-
- `
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
- `list_notable_records`
|
|
251
|
+
### Wiki / Docs
|
|
252
|
+
- `list_wiki_workspaces` / `get_wiki_workspace`
|
|
253
|
+
- `list_wiki_nodes` / `get_wiki_node`
|
|
254
|
+
- `create_wiki_doc` / `delete_wiki_doc` / `rename_wiki_doc`
|
|
255
|
+
- `get_wiki_doc_content` / `update_wiki_doc_content`
|
|
256
|
+
- `search_wiki` — name search (BFS, no index needed)
|
|
257
|
+
- `search_wiki_content` — full-text search (requires index)
|
|
258
|
+
- `refresh_search_index` — rebuild search index
|
|
259
|
+
|
|
260
|
+
### AI 表格 (Notable)
|
|
261
|
+
- `list_notable_sheets` / `list_notable_records`
|
|
262
|
+
- `create_notable_record` / `update_notable_record` / `delete_notable_record`
|
|
263
|
+
- `create_notable_sheet` / `delete_notable_sheet`
|
|
264
|
+
|
|
265
|
+
### Organization
|
|
266
|
+
- `list_departments` / `get_department_users` / `get_user_info`
|
|
257
267
|
|
|
258
268
|
---
|
|
259
269
|
|
|
@@ -301,8 +311,9 @@ Please refer to DingTalk Open Platform documentation for the latest permission n
|
|
|
301
311
|
|
|
302
312
|
- This is a community-maintained complement, not an official DingTalk project
|
|
303
313
|
- Some APIs require enterprise approval on the DingTalk side
|
|
304
|
-
- `search_wiki`
|
|
305
|
-
-
|
|
314
|
+
- `search_wiki` traverses directory tree by name (always available)
|
|
315
|
+
- `search_wiki_content` uses MiniSearch + SQLite for full-text search, requires `refresh_search_index` first
|
|
316
|
+
- Search index updates on write operations (create/update/rename/delete) via async hooks
|
|
306
317
|
- Notable / `.able` support currently covers sheets and records, not arbitrary document-body export
|
|
307
318
|
|
|
308
319
|
---
|
package/README.zh-CN.md
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
## 仓库要点
|
|
16
16
|
|
|
17
17
|
- **官方 MCP 有空白**:没有覆盖 Wiki / Docs 读写
|
|
18
|
-
- **这个项目补上了**:支持 workspace、node
|
|
18
|
+
- **这个项目补上了**:支持 workspace、node 浏览、文档 CRUD 和全文搜索
|
|
19
|
+
- **本地搜索索引**:MiniSearch + SQLite — 全文搜索文档和 Notable 表格内容
|
|
19
20
|
- **MCP 兼容**:兼容 stdio 模式的 MCP client
|
|
20
21
|
- **Agent 友好**:自带 `SKILL.md`,可直接作为 skill 复用
|
|
21
22
|
|
|
@@ -124,6 +125,8 @@ node index.js
|
|
|
124
125
|
| 浏览 workspace | 未覆盖 | ✅ |
|
|
125
126
|
| 浏览 nodes / 目录 | 未覆盖 | ✅ |
|
|
126
127
|
| 读取 Notable / `.able` 记录 | 未覆盖 | ✅ |
|
|
128
|
+
| 名称搜索 | 未覆盖 | ✅ |
|
|
129
|
+
| 全文内容搜索 | 未覆盖 | ✅ MiniSearch + SQLite |
|
|
127
130
|
| MCP 客户端兼容性 | 官方范围内 | ✅ stdio 兼容 |
|
|
128
131
|
| OpenClaw skill 化复用 | 无 | ✅ 自带 `SKILL.md` |
|
|
129
132
|
|
|
@@ -142,7 +145,9 @@ node index.js
|
|
|
142
145
|
- `WORKBOOK`
|
|
143
146
|
- `MIND`
|
|
144
147
|
- `FOLDER`
|
|
145
|
-
-
|
|
148
|
+
- 按名称搜索(`search_wiki`,BFS 遍历目录树,无需索引)
|
|
149
|
+
- 全文内容搜索(`search_wiki_content`,MiniSearch + SQLite 索引)
|
|
150
|
+
- 搜索索引管理(`refresh_search_index`)
|
|
146
151
|
- 通过官方 API 读取 Notable / `.able` 的数据表和记录
|
|
147
152
|
|
|
148
153
|
### 组织架构
|
|
@@ -236,17 +241,22 @@ mcporter call --stdio "node ./index.js" create_wiki_doc workspace_id="your_works
|
|
|
236
241
|
|
|
237
242
|
## 可用 MCP 工具
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
- `get_wiki_workspace`
|
|
241
|
-
- `list_wiki_nodes`
|
|
242
|
-
- `
|
|
243
|
-
- `
|
|
244
|
-
- `search_wiki`
|
|
245
|
-
- `
|
|
246
|
-
- `
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
- `list_notable_records`
|
|
244
|
+
### Wiki / Docs
|
|
245
|
+
- `list_wiki_workspaces` / `get_wiki_workspace`
|
|
246
|
+
- `list_wiki_nodes` / `get_wiki_node`
|
|
247
|
+
- `create_wiki_doc` / `delete_wiki_doc` / `rename_wiki_doc`
|
|
248
|
+
- `get_wiki_doc_content` / `update_wiki_doc_content`
|
|
249
|
+
- `search_wiki` — 名称搜索(BFS,无需索引)
|
|
250
|
+
- `search_wiki_content` — 全文搜索(需先建索引)
|
|
251
|
+
- `refresh_search_index` — 重建搜索索引
|
|
252
|
+
|
|
253
|
+
### AI 表格(Notable)
|
|
254
|
+
- `list_notable_sheets` / `list_notable_records`
|
|
255
|
+
- `create_notable_record` / `update_notable_record` / `delete_notable_record`
|
|
256
|
+
- `create_notable_sheet` / `delete_notable_sheet`
|
|
257
|
+
|
|
258
|
+
### 组织架构
|
|
259
|
+
- `list_departments` / `get_department_users` / `get_user_info`
|
|
250
260
|
|
|
251
261
|
---
|
|
252
262
|
|
|
@@ -295,9 +305,9 @@ mcporter call --stdio "node ./index.js" create_wiki_doc workspace_id="your_works
|
|
|
295
305
|
|
|
296
306
|
- 本项目是社区补充实现,不是钉钉官方项目
|
|
297
307
|
- 部分 API 需要企业侧权限审批
|
|
298
|
-
- `search_wiki`
|
|
299
|
-
-
|
|
300
|
-
-
|
|
308
|
+
- `search_wiki` 通过 BFS 遍历目录树按名称匹配(始终可用)
|
|
309
|
+
- `search_wiki_content` 使用 MiniSearch + SQLite 全文搜索,需先运行 `refresh_search_index`
|
|
310
|
+
- 搜索索引在写入操作(创建/更新/重命名/删除)时通过异步 hook 自动更新
|
|
301
311
|
|
|
302
312
|
---
|
|
303
313
|
|
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 配置
|
|
@@ -21,6 +23,7 @@ const path = require('path');
|
|
|
21
23
|
const os = require('os');
|
|
22
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;
|
|
@@ -378,6 +596,7 @@ class DingTalkClient {
|
|
|
378
596
|
}
|
|
379
597
|
|
|
380
598
|
const dingtalk = new DingTalkClient();
|
|
599
|
+
const wikiIndex = new WikiSearchIndex();
|
|
381
600
|
|
|
382
601
|
// MCP Server 定义
|
|
383
602
|
const server = new Server(
|
|
@@ -503,7 +722,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
503
722
|
},
|
|
504
723
|
{
|
|
505
724
|
name: 'search_wiki',
|
|
506
|
-
description: '
|
|
725
|
+
description: '按名称搜索知识库中的文档和文件夹(遍历目录树,无需索引即可使用)',
|
|
726
|
+
inputSchema: {
|
|
727
|
+
type: 'object',
|
|
728
|
+
properties: {
|
|
729
|
+
keyword: {
|
|
730
|
+
type: 'string',
|
|
731
|
+
description: '搜索关键词'
|
|
732
|
+
},
|
|
733
|
+
workspace_id: {
|
|
734
|
+
type: 'string',
|
|
735
|
+
description: '指定知识库 ID(可选,不传则搜索所有知识库)'
|
|
736
|
+
},
|
|
737
|
+
max_results: {
|
|
738
|
+
type: 'number',
|
|
739
|
+
description: '返回条数上限(默认 20,最大 50)'
|
|
740
|
+
},
|
|
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 建立索引)',
|
|
507
765
|
inputSchema: {
|
|
508
766
|
type: 'object',
|
|
509
767
|
properties: {
|
|
@@ -1052,6 +1310,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1052
1310
|
lines.push(`📂 Workspace ID: ${doc.workspaceId}`);
|
|
1053
1311
|
}
|
|
1054
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
|
+
|
|
1055
1329
|
return {
|
|
1056
1330
|
content: [{
|
|
1057
1331
|
type: 'text',
|
|
@@ -1125,11 +1399,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1125
1399
|
};
|
|
1126
1400
|
}
|
|
1127
1401
|
|
|
1128
|
-
const MAX_RESULTS = Math.min(max_results, 50);
|
|
1129
1402
|
if (operator_id) {
|
|
1130
1403
|
dingtalk.setOperatorId(operator_id);
|
|
1131
1404
|
}
|
|
1132
1405
|
|
|
1406
|
+
const MAX_RESULTS = Math.min(max_results, 50);
|
|
1133
1407
|
const wsResult = await dingtalk.wikiRequest('workspaces');
|
|
1134
1408
|
let workspaces = wsResult.workspaces || [];
|
|
1135
1409
|
if (workspace_id) {
|
|
@@ -1144,70 +1418,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1144
1418
|
}
|
|
1145
1419
|
|
|
1146
1420
|
const matchedNodes = [];
|
|
1147
|
-
|
|
1148
1421
|
for (const ws of workspaces) {
|
|
1149
1422
|
if (matchedNodes.length >= MAX_RESULTS) break;
|
|
1150
|
-
|
|
1151
1423
|
const queue = [ws.rootNodeId];
|
|
1152
1424
|
const visited = new Set();
|
|
1153
|
-
|
|
1154
1425
|
while (queue.length > 0 && matchedNodes.length < MAX_RESULTS) {
|
|
1155
1426
|
const dentryId = queue.shift();
|
|
1156
1427
|
if (!dentryId || visited.has(dentryId)) continue;
|
|
1157
1428
|
visited.add(dentryId);
|
|
1158
|
-
|
|
1159
1429
|
try {
|
|
1160
1430
|
const result = await dingtalk.docRequest('GET', `/v2.0/doc/spaces/${ws.workspaceId}/directories`, {
|
|
1161
|
-
operatorId: operator_id || null,
|
|
1162
1431
|
extraParams: dentryId !== ws.rootNodeId ? { dentryId, maxResults: 500 } : { maxResults: 500 }
|
|
1163
1432
|
});
|
|
1164
|
-
|
|
1165
1433
|
const children = result.children || [];
|
|
1166
1434
|
for (const child of children) {
|
|
1167
1435
|
const name = child.name || '';
|
|
1168
1436
|
if (name.includes(keyword)) {
|
|
1169
|
-
matchedNodes.push({
|
|
1170
|
-
name,
|
|
1171
|
-
nodeId: child.dentryId || child.nodeId || child.id,
|
|
1172
|
-
workspaceId: ws.workspaceId,
|
|
1173
|
-
workspaceName: ws.name,
|
|
1174
|
-
type: child.contentType === 'folder' ? '文件夹' : '文档',
|
|
1175
|
-
url: child.url || '',
|
|
1176
|
-
hasChildren: child.hasChildren
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
if (child.hasChildren && matchedNodes.length < MAX_RESULTS) {
|
|
1180
|
-
const childId = child.dentryId || child.nodeId || child.id;
|
|
1181
|
-
if (childId && !visited.has(childId)) {
|
|
1182
|
-
queue.push(childId);
|
|
1183
|
-
}
|
|
1437
|
+
matchedNodes.push({ name, nodeId: child.dentryId || child.nodeId || child.id, workspaceId: ws.workspaceId, workspaceName: ws.name, type: child.contentType === 'folder' ? '文件夹' : '文档', url: child.url || '' });
|
|
1184
1438
|
}
|
|
1439
|
+
const childId = child.dentryId || child.nodeId || child.id;
|
|
1440
|
+
if (child.hasChildren && childId && !visited.has(childId)) queue.push(childId);
|
|
1185
1441
|
}
|
|
1186
|
-
} catch (e) {
|
|
1187
|
-
// 单个节点遍历失败跳过,不中断整体搜索
|
|
1188
|
-
}
|
|
1442
|
+
} catch (e) { /* skip */ }
|
|
1189
1443
|
}
|
|
1190
1444
|
}
|
|
1191
1445
|
|
|
1192
|
-
let output = `🔍 搜索 "${keyword}" (${matchedNodes.length}
|
|
1446
|
+
let output = `🔍 搜索 "${keyword}" (${matchedNodes.length}条,按名称匹配)\n\n`;
|
|
1193
1447
|
matchedNodes.slice(0, MAX_RESULTS).forEach((item, i) => {
|
|
1194
1448
|
const icon = item.type === '文件夹' ? '📁' : '📄';
|
|
1195
|
-
output += `${i + 1}. ${icon} ${item.name}\n`;
|
|
1196
|
-
output += ` 知识库: ${item.workspaceName} (${item.workspaceId})\n`;
|
|
1449
|
+
output += `${i + 1}. ${icon} ${item.name}\n 知识库: ${item.workspaceName} (${item.workspaceId})\n`;
|
|
1197
1450
|
if (item.url) output += ` 链接: ${item.url}\n`;
|
|
1198
1451
|
output += '\n';
|
|
1199
1452
|
});
|
|
1200
|
-
|
|
1201
1453
|
if (matchedNodes.length === 0) {
|
|
1202
1454
|
output += '没有找到匹配的文档或文件夹。\n';
|
|
1203
|
-
output += `\n💡 此方式通过遍历目录树按名称匹配,非全文搜索。`;
|
|
1204
|
-
output += `如需全文搜索,请在钉钉客户端中操作:\n`;
|
|
1205
|
-
output += `https://alidocs.dingtalk.com/i/search?keyword=${encodeURIComponent(keyword)}`;
|
|
1206
1455
|
}
|
|
1207
1456
|
|
|
1208
|
-
return {
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
+
};
|
|
1467
|
+
}
|
|
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 }] };
|
|
1211
1507
|
}
|
|
1212
1508
|
|
|
1213
1509
|
case 'list_departments': {
|
|
@@ -1292,6 +1588,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1292
1588
|
operatorId: operator_id || null,
|
|
1293
1589
|
data: { content, contentType: 'markdown' }
|
|
1294
1590
|
});
|
|
1591
|
+
setImmediate(() => { wikiIndex.update(docKey, { content }); });
|
|
1295
1592
|
return {
|
|
1296
1593
|
content: [{ type: 'text', text: '✅ 文档内容已更新' }]
|
|
1297
1594
|
};
|
|
@@ -1306,6 +1603,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1306
1603
|
operatorId: operator_id || null,
|
|
1307
1604
|
data: { name }
|
|
1308
1605
|
});
|
|
1606
|
+
setImmediate(() => { wikiIndex.update(node_id, { title: name }); });
|
|
1309
1607
|
return {
|
|
1310
1608
|
content: [{ type: 'text', text: `✅ 文档已重命名为: ${name}` }]
|
|
1311
1609
|
};
|
|
@@ -1319,6 +1617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1319
1617
|
await dingtalk.docRequest('DELETE', `/v1.0/doc/workspaces/${workspace_id}/docs/${node_id}`, {
|
|
1320
1618
|
operatorId: operator_id || null
|
|
1321
1619
|
});
|
|
1620
|
+
setImmediate(() => { wikiIndex.remove(node_id); });
|
|
1322
1621
|
return {
|
|
1323
1622
|
content: [{
|
|
1324
1623
|
type: 'text',
|
|
@@ -1383,6 +1682,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1383
1682
|
operatorId: operator_id || null,
|
|
1384
1683
|
data: { records }
|
|
1385
1684
|
});
|
|
1685
|
+
setImmediate(() => reindexNotableBase(base_id));
|
|
1386
1686
|
return {
|
|
1387
1687
|
content: [{
|
|
1388
1688
|
type: 'text',
|
|
@@ -1397,6 +1697,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1397
1697
|
operatorId: operator_id || null,
|
|
1398
1698
|
data: { records }
|
|
1399
1699
|
});
|
|
1700
|
+
setImmediate(() => reindexNotableBase(base_id));
|
|
1400
1701
|
return {
|
|
1401
1702
|
content: [{
|
|
1402
1703
|
type: 'text',
|
|
@@ -1411,6 +1712,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1411
1712
|
operatorId: operator_id || null,
|
|
1412
1713
|
data: { recordIds: record_ids }
|
|
1413
1714
|
});
|
|
1715
|
+
setImmediate(() => reindexNotableBase(base_id));
|
|
1414
1716
|
return {
|
|
1415
1717
|
content: [{
|
|
1416
1718
|
type: 'text',
|
|
@@ -1450,6 +1752,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1450
1752
|
};
|
|
1451
1753
|
}
|
|
1452
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
|
+
|
|
1453
1769
|
default:
|
|
1454
1770
|
throw new Error(`未知工具: ${name}`);
|
|
1455
1771
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dingtalk-wiki",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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.
|
|
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
|