dingtalk-wiki 1.2.15 → 1.2.17
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 +93 -467
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -213,7 +213,28 @@ async function rebuildSearchIndex() {
|
|
|
213
213
|
const blocks = await dingtalk.docRequest('GET', `/v1.0/doc/suites/documents/${childId}/blocks`);
|
|
214
214
|
const blockData = blocks.result?.data || [];
|
|
215
215
|
content = blockData.map(b => extractBlockText(b)).join('\n\n');
|
|
216
|
-
} catch (e) { /* content unavailable */ }
|
|
216
|
+
} catch (e) { /* content unavailable via blocks */ }
|
|
217
|
+
|
|
218
|
+
// Fallback: 存储 v1.0 下载 API(对上传的 .md 等文件有效)
|
|
219
|
+
if (!content && child.spaceId) {
|
|
220
|
+
try {
|
|
221
|
+
const token = await dingtalk.getAccessToken();
|
|
222
|
+
const dlRes = await axios({
|
|
223
|
+
method: 'POST',
|
|
224
|
+
url: `${DINGTALK_API_V2}/v1.0/storage/spaces/${child.spaceId}/dentries/${child.dentryId}/downloadInfos/query`,
|
|
225
|
+
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
226
|
+
params: { unionId: await dingtalk.resolveOperatorId(null) },
|
|
227
|
+
data: {}
|
|
228
|
+
});
|
|
229
|
+
const data = dlRes.data;
|
|
230
|
+
const downloadUrl = data.downloadInfo?.resourceUrl || data.resourceUrl || data.downloadUrl || data.url;
|
|
231
|
+
const downloadHeaders = data.downloadInfo?.headers || data.headers || {};
|
|
232
|
+
if (downloadUrl) {
|
|
233
|
+
const contentRes = await axios({ method: 'GET', url: downloadUrl, headers: downloadHeaders, responseType: 'text' });
|
|
234
|
+
content = contentRes.data;
|
|
235
|
+
}
|
|
236
|
+
} catch (e) { /* storage v1.0 unavailable */ }
|
|
237
|
+
}
|
|
217
238
|
|
|
218
239
|
if (!content) {
|
|
219
240
|
try {
|
|
@@ -726,28 +747,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
726
747
|
required: ['node_id']
|
|
727
748
|
}
|
|
728
749
|
},
|
|
729
|
-
{
|
|
730
|
-
name: 'debug_resolve_doc_key',
|
|
731
|
-
description: '[调试] 诊断 docKey 解析问题,测试所有可能的 API 路径并返回原始响应',
|
|
732
|
-
inputSchema: {
|
|
733
|
-
type: 'object',
|
|
734
|
-
properties: {
|
|
735
|
-
doc_key: {
|
|
736
|
-
type: 'string',
|
|
737
|
-
description: '文档标识(dentryUuid / docKey / nodeId / URL)'
|
|
738
|
-
},
|
|
739
|
-
workspace_id: {
|
|
740
|
-
type: 'string',
|
|
741
|
-
description: '知识库 ID(可选)'
|
|
742
|
-
},
|
|
743
|
-
operator_id: {
|
|
744
|
-
type: 'string',
|
|
745
|
-
description: '操作者 unionid(不传则使用默认用户)'
|
|
746
|
-
}
|
|
747
|
-
},
|
|
748
|
-
required: ['doc_key']
|
|
749
|
-
}
|
|
750
|
-
},
|
|
751
750
|
{
|
|
752
751
|
name: 'search_wiki',
|
|
753
752
|
description: '按名称搜索知识库中的文档和文件夹(遍历目录树,无需索引即可使用)',
|
|
@@ -1308,418 +1307,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1308
1307
|
};
|
|
1309
1308
|
}
|
|
1310
1309
|
|
|
1311
|
-
case 'debug_resolve_doc_key': {
|
|
1312
|
-
const { doc_key: docKey, workspace_id: workspaceId, operator_id } = args;
|
|
1313
|
-
if (operator_id) {
|
|
1314
|
-
dingtalk.setOperatorId(operator_id);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const token = await dingtalk.getAccessToken();
|
|
1318
|
-
const opId = await dingtalk.resolveOperatorId(operator_id || null);
|
|
1319
|
-
const steps = [];
|
|
1320
|
-
const input = String(docKey);
|
|
1321
|
-
|
|
1322
|
-
steps.push({ step: '输入', result: input });
|
|
1323
|
-
|
|
1324
|
-
// Step 1: URL extraction
|
|
1325
|
-
const urlMatch = input.match(/\/i\/nodes\/([^\/\?#]+)/);
|
|
1326
|
-
const urlExtracted = urlMatch ? urlMatch[1] : null;
|
|
1327
|
-
steps.push({ step: 'URL 提取', result: urlExtracted || '无匹配', raw: urlMatch ? urlMatch[0] : null });
|
|
1328
|
-
|
|
1329
|
-
// Step 2: wiki/nodes API
|
|
1330
|
-
let wikiNodeResponse = null;
|
|
1331
|
-
let wikiWsId = null;
|
|
1332
|
-
try {
|
|
1333
|
-
const nodeRes = await axios({
|
|
1334
|
-
method: 'GET',
|
|
1335
|
-
url: `${DINGTALK_API_V2}/v2.0/wiki/nodes/${input}`,
|
|
1336
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1337
|
-
params: { operatorId: opId }
|
|
1338
|
-
});
|
|
1339
|
-
wikiNodeResponse = nodeRes.data;
|
|
1340
|
-
wikiWsId = wikiNodeResponse.workspaceId || wikiNodeResponse.node?.workspaceId || null;
|
|
1341
|
-
const node = wikiNodeResponse;
|
|
1342
|
-
steps.push({
|
|
1343
|
-
step: 'wiki/nodes API',
|
|
1344
|
-
result: '成功',
|
|
1345
|
-
raw: node
|
|
1346
|
-
});
|
|
1347
|
-
} catch (e) {
|
|
1348
|
-
steps.push({
|
|
1349
|
-
step: 'wiki/nodes API',
|
|
1350
|
-
result: '失败',
|
|
1351
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
const effectiveWsId = workspaceId || wikiWsId;
|
|
1356
|
-
|
|
1357
|
-
// Step 3: doc metadata API (GET /v1.0/doc/suites/documents/{docKey})
|
|
1358
|
-
try {
|
|
1359
|
-
const metaRes = await axios({
|
|
1360
|
-
method: 'GET',
|
|
1361
|
-
url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}`,
|
|
1362
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1363
|
-
params: { operatorId: opId }
|
|
1364
|
-
});
|
|
1365
|
-
steps.push({
|
|
1366
|
-
step: 'doc metadata API (GET /documents/{docKey})',
|
|
1367
|
-
result: '成功',
|
|
1368
|
-
raw: metaRes.data
|
|
1369
|
-
});
|
|
1370
|
-
} catch (e) {
|
|
1371
|
-
steps.push({
|
|
1372
|
-
step: 'doc metadata API (GET /documents/{docKey})',
|
|
1373
|
-
result: '失败',
|
|
1374
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
// Step 4: blocks API
|
|
1379
|
-
try {
|
|
1380
|
-
const blocksRes = await axios({
|
|
1381
|
-
method: 'GET',
|
|
1382
|
-
url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}/blocks`,
|
|
1383
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1384
|
-
params: { operatorId: opId }
|
|
1385
|
-
});
|
|
1386
|
-
const blockCount = blocksRes.data?.result?.data?.length || 0;
|
|
1387
|
-
steps.push({
|
|
1388
|
-
step: 'blocks API',
|
|
1389
|
-
result: `成功 (${blockCount} 个 block)`,
|
|
1390
|
-
raw: blocksRes.data
|
|
1391
|
-
});
|
|
1392
|
-
} catch (e) {
|
|
1393
|
-
steps.push({
|
|
1394
|
-
step: 'blocks API',
|
|
1395
|
-
result: '失败',
|
|
1396
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// Step 5: overwriteContent API (test with empty content — don't actually write)
|
|
1401
|
-
try {
|
|
1402
|
-
const overwriteRes = await axios({
|
|
1403
|
-
method: 'POST',
|
|
1404
|
-
url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}/overwriteContent`,
|
|
1405
|
-
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
1406
|
-
params: { operatorId: opId },
|
|
1407
|
-
data: { content: 'test', contentType: 'markdown' }
|
|
1408
|
-
});
|
|
1409
|
-
steps.push({
|
|
1410
|
-
step: 'overwriteContent API',
|
|
1411
|
-
result: '成功(可写入)',
|
|
1412
|
-
raw: overwriteRes.data
|
|
1413
|
-
});
|
|
1414
|
-
} catch (e) {
|
|
1415
|
-
steps.push({
|
|
1416
|
-
step: 'overwriteContent API',
|
|
1417
|
-
result: '失败',
|
|
1418
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
// Step 6: search docs API (v1.0/doc/docs) — 需要 workspaceId + keyword
|
|
1423
|
-
if (effectiveWsId) {
|
|
1424
|
-
// Try without keyword first (might list all)
|
|
1425
|
-
try {
|
|
1426
|
-
const searchRes = await axios({
|
|
1427
|
-
method: 'GET',
|
|
1428
|
-
url: `${DINGTALK_API_V2}/v1.0/doc/docs`,
|
|
1429
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1430
|
-
params: { operatorId: opId, workspaceId: effectiveWsId, maxResults: 50 }
|
|
1431
|
-
});
|
|
1432
|
-
const docs = searchRes.data?.docs || [];
|
|
1433
|
-
const matched = docs.find(d => d.nodeBO?.nodeId === input || d.nodeBO?.dentryUuid === input || d.docKey === input || d.nodeBO?.dentryId === input);
|
|
1434
|
-
steps.push({
|
|
1435
|
-
step: `doc search API (workspace=${effectiveWsId}, no keyword)`,
|
|
1436
|
-
result: `成功 (${docs.length} 篇文档)` + (matched ? ', 找到匹配!' : ', 未找到匹配'),
|
|
1437
|
-
raw: docs.length > 0 ? { count: docs.length, firstDocDocKey: docs[0]?.docKey || '(无)', firstDocNodeId: docs[0]?.nodeBO?.nodeId || '(无)', firstDocName: docs[0]?.name || '(无)' } : '空结果'
|
|
1438
|
-
});
|
|
1439
|
-
} catch (e) {
|
|
1440
|
-
steps.push({
|
|
1441
|
-
step: `doc search API (workspace=${effectiveWsId}, no keyword)`,
|
|
1442
|
-
result: '失败',
|
|
1443
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
// Try with keyword from wiki/nodes name if available
|
|
1447
|
-
const wikiName = wikiNodeResponse?.node?.name || wikiNodeResponse?.name || '';
|
|
1448
|
-
if (wikiName) {
|
|
1449
|
-
try {
|
|
1450
|
-
const searchRes = await axios({
|
|
1451
|
-
method: 'GET',
|
|
1452
|
-
url: `${DINGTALK_API_V2}/v1.0/doc/docs`,
|
|
1453
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1454
|
-
params: { operatorId: opId, workspaceId: effectiveWsId, keyword: wikiName.replace(/\.\w+$/, ''), maxResults: 10 }
|
|
1455
|
-
});
|
|
1456
|
-
const docs = searchRes.data?.docs || [];
|
|
1457
|
-
steps.push({
|
|
1458
|
-
step: `doc search API (keyword="${wikiName}")`,
|
|
1459
|
-
result: `成功 (${docs.length} 篇)`,
|
|
1460
|
-
raw: docs.length > 0 ? docs.map(d => ({ docKey: d.docKey || '(无)', name: d.name || '(无)', nodeId: d.nodeBO?.nodeId || '(无)', dentryUuid: d.nodeBO?.dentryUuid || '(无)' })) : '空结果'
|
|
1461
|
-
});
|
|
1462
|
-
} catch (e) {
|
|
1463
|
-
steps.push({
|
|
1464
|
-
step: `doc search API (keyword="${wikiName}")`,
|
|
1465
|
-
result: '失败',
|
|
1466
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
} else {
|
|
1471
|
-
steps.push({ step: 'doc search API', result: '跳过(无法获取 workspace_id)' });
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
// Step 7: 在知识库目录中查找此节点 + 查看是否有 docKey 字段
|
|
1475
|
-
if (effectiveWsId) {
|
|
1476
|
-
try {
|
|
1477
|
-
const dirRes = await axios({
|
|
1478
|
-
method: 'GET',
|
|
1479
|
-
url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${effectiveWsId}/directories`,
|
|
1480
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1481
|
-
params: { operatorId: opId, maxResults: 500 }
|
|
1482
|
-
});
|
|
1483
|
-
const children = dirRes.data?.children || [];
|
|
1484
|
-
const match = children.find(c => c.dentryUuid === input || c.dentryId === input || c.id === input);
|
|
1485
|
-
// Show first few children's available fields so user can see what fields exist
|
|
1486
|
-
const sampleKeys = children.length > 0 ? Object.keys(children[0]) : [];
|
|
1487
|
-
steps.push({
|
|
1488
|
-
step: `知识库目录 (workspace=${effectiveWsId})`,
|
|
1489
|
-
result: `${children.length} 个子节点` + (match ? ', 找到匹配!' : ', 未找到匹配'),
|
|
1490
|
-
raw: match ? match : { sampleFields: sampleKeys, firstChildSample: children[0] || null }
|
|
1491
|
-
});
|
|
1492
|
-
} catch (e) {
|
|
1493
|
-
steps.push({
|
|
1494
|
-
step: `知识库目录 (workspace=${effectiveWsId})`,
|
|
1495
|
-
result: '失败',
|
|
1496
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
// Step 8: 用 workspaceId 再次尝试 overwriteContent(确认文档套件是否完全不可用)
|
|
1502
|
-
if (effectiveWsId) {
|
|
1503
|
-
try {
|
|
1504
|
-
const overwriteRes = await axios({
|
|
1505
|
-
method: 'POST',
|
|
1506
|
-
url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}/overwriteContent`,
|
|
1507
|
-
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
1508
|
-
params: { operatorId: opId },
|
|
1509
|
-
data: { content: 'test', contentType: 'markdown' }
|
|
1510
|
-
});
|
|
1511
|
-
steps.push({
|
|
1512
|
-
step: 'overwriteContent(带 workspaceId)',
|
|
1513
|
-
result: '成功',
|
|
1514
|
-
raw: overwriteRes.data
|
|
1515
|
-
});
|
|
1516
|
-
} catch (e) {
|
|
1517
|
-
steps.push({
|
|
1518
|
-
step: 'overwriteContent(带 workspaceId)',
|
|
1519
|
-
result: '失败',
|
|
1520
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// Step 9: 递归遍历目录树查找文档 + 存储API下载内容
|
|
1526
|
-
let foundDentry = null;
|
|
1527
|
-
let foundSpaceId = null;
|
|
1528
|
-
if (effectiveWsId) {
|
|
1529
|
-
try {
|
|
1530
|
-
// BFS 遍历目录树查找 dentry
|
|
1531
|
-
const queue = [''];
|
|
1532
|
-
const visited = new Set();
|
|
1533
|
-
while (queue.length > 0 && !foundDentry) {
|
|
1534
|
-
const parentId = queue.shift();
|
|
1535
|
-
if (visited.has(parentId)) continue;
|
|
1536
|
-
visited.add(parentId);
|
|
1537
|
-
try {
|
|
1538
|
-
const dirParams = { operatorId: opId, maxResults: 500 };
|
|
1539
|
-
if (parentId) dirParams.dentryId = parentId;
|
|
1540
|
-
const dirRes = await axios({
|
|
1541
|
-
method: 'GET',
|
|
1542
|
-
url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${effectiveWsId}/directories`,
|
|
1543
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1544
|
-
params: dirParams
|
|
1545
|
-
});
|
|
1546
|
-
const children = dirRes.data?.children || [];
|
|
1547
|
-
for (const child of children) {
|
|
1548
|
-
if (child.dentryUuid === input || child.dentryId === input) {
|
|
1549
|
-
foundDentry = child;
|
|
1550
|
-
foundSpaceId = child.spaceId;
|
|
1551
|
-
break;
|
|
1552
|
-
}
|
|
1553
|
-
if (child.hasChildren && child.dentryId && !visited.has(child.dentryId)) {
|
|
1554
|
-
queue.push(child.dentryId);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
} catch (e) { /* skip */ }
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
if (foundDentry) {
|
|
1561
|
-
steps.push({
|
|
1562
|
-
step: 'BFS 目录遍历找到文档',
|
|
1563
|
-
result: `spaceId=${foundSpaceId}, dentryId=${foundDentry.dentryId}, name=${foundDentry.name}`,
|
|
1564
|
-
raw: foundDentry
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
// 尝试存储 API 获取 dentry 详情(含 docKey)
|
|
1568
|
-
try {
|
|
1569
|
-
const dentryRes = await axios({
|
|
1570
|
-
method: 'GET',
|
|
1571
|
-
url: `${DINGTALK_API_V2}/v2.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}`,
|
|
1572
|
-
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1573
|
-
params: { operatorId: opId }
|
|
1574
|
-
});
|
|
1575
|
-
steps.push({
|
|
1576
|
-
step: '存储 API dentry 详情',
|
|
1577
|
-
result: '成功',
|
|
1578
|
-
raw: dentryRes.data
|
|
1579
|
-
});
|
|
1580
|
-
} catch (e) {
|
|
1581
|
-
steps.push({
|
|
1582
|
-
step: '存储 API dentry 详情',
|
|
1583
|
-
result: '失败',
|
|
1584
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1585
|
-
});
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// 尝试存储 API 下载
|
|
1589
|
-
try {
|
|
1590
|
-
const dlRes = await axios({
|
|
1591
|
-
method: 'POST',
|
|
1592
|
-
url: `${DINGTALK_API_V2}/v2.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}/downloadInfos/query`,
|
|
1593
|
-
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
1594
|
-
params: { operatorId: opId },
|
|
1595
|
-
data: {}
|
|
1596
|
-
});
|
|
1597
|
-
steps.push({
|
|
1598
|
-
step: '存储 API 获取下载链接',
|
|
1599
|
-
result: '成功',
|
|
1600
|
-
raw: dlRes.data
|
|
1601
|
-
});
|
|
1602
|
-
// 尝试从下载链接获取内容
|
|
1603
|
-
const dlInfo = dlRes.data;
|
|
1604
|
-
const downloadUrl = dlInfo.headerSignatureInfo?.resourceUrls?.[0] || dlInfo.downloadUrl || dlInfo.url;
|
|
1605
|
-
if (downloadUrl) {
|
|
1606
|
-
try {
|
|
1607
|
-
const contentRes = await axios({ method: 'GET', url: downloadUrl, responseType: 'text' });
|
|
1608
|
-
steps.push({
|
|
1609
|
-
step: '存储 API 下载内容',
|
|
1610
|
-
result: `成功 (${contentRes.data.length} 字符)`,
|
|
1611
|
-
raw: contentRes.data.slice(0, 500) + (contentRes.data.length > 500 ? '\n... (截断)' : '')
|
|
1612
|
-
});
|
|
1613
|
-
} catch (dlErr) {
|
|
1614
|
-
steps.push({
|
|
1615
|
-
step: '存储 API 下载内容',
|
|
1616
|
-
result: '失败',
|
|
1617
|
-
raw: dlErr.message
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
} catch (e) {
|
|
1622
|
-
steps.push({
|
|
1623
|
-
step: '存储 API 获取下载链接',
|
|
1624
|
-
result: '失败',
|
|
1625
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
} else {
|
|
1629
|
-
steps.push({ step: 'BFS 目录遍历', result: '未找到匹配(文档可能在深层目录)' });
|
|
1630
|
-
}
|
|
1631
|
-
} catch (e) {
|
|
1632
|
-
steps.push({ step: 'BFS 目录遍历', result: '失败', raw: e.message });
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// Step 10: 存储 API - 文件下载(.md 文件内容 = 文件下载)
|
|
1637
|
-
if (foundSpaceId) {
|
|
1638
|
-
// 方式1: storage/spaces/files/{dentryUuid}
|
|
1639
|
-
for (const path of [
|
|
1640
|
-
`/v2.0/storage/spaces/files/${input}/downloadInfos/query`,
|
|
1641
|
-
`/v2.0/storage/spaces/files/${foundDentry.dentryId}/downloadInfos/query`,
|
|
1642
|
-
]) {
|
|
1643
|
-
try {
|
|
1644
|
-
const dlRes = await axios({
|
|
1645
|
-
method: 'POST',
|
|
1646
|
-
url: `${DINGTALK_API_V2}${path}`,
|
|
1647
|
-
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
1648
|
-
params: { operatorId: opId },
|
|
1649
|
-
data: {}
|
|
1650
|
-
});
|
|
1651
|
-
steps.push({
|
|
1652
|
-
step: `存储下载 (POST ${path})`,
|
|
1653
|
-
result: '成功',
|
|
1654
|
-
raw: dlRes.data
|
|
1655
|
-
});
|
|
1656
|
-
} catch (e) {
|
|
1657
|
-
steps.push({
|
|
1658
|
-
step: `存储下载 (POST ${path})`,
|
|
1659
|
-
result: '失败',
|
|
1660
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
// 方式2: storage/spaces/download 的 GET 变体
|
|
1665
|
-
for (const path of [
|
|
1666
|
-
`/v2.0/storage/spaces/files/${input}/downloadInfo`,
|
|
1667
|
-
`/v2.0/storage/spaces/${foundSpaceId}/files/${input}/downloadInfos/query`,
|
|
1668
|
-
`/v2.0/storage/spaces/${foundSpaceId}/files/${foundDentry.dentryId}/downloadInfos/query`,
|
|
1669
|
-
]) {
|
|
1670
|
-
try {
|
|
1671
|
-
const dlRes = await axios({
|
|
1672
|
-
method: 'POST',
|
|
1673
|
-
url: `${DINGTALK_API_V2}${path}`,
|
|
1674
|
-
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
1675
|
-
params: { operatorId: opId },
|
|
1676
|
-
data: {}
|
|
1677
|
-
});
|
|
1678
|
-
steps.push({
|
|
1679
|
-
step: `存储下载 (POST ${path})`,
|
|
1680
|
-
result: '成功',
|
|
1681
|
-
raw: dlRes.data
|
|
1682
|
-
});
|
|
1683
|
-
} catch (e) {
|
|
1684
|
-
steps.push({
|
|
1685
|
-
step: `存储下载 (POST ${path})`,
|
|
1686
|
-
result: '失败',
|
|
1687
|
-
raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
const lines = ['🔍 docKey 诊断报告', '', `输入: ${input}`, `operatorId: ${opId}`, ''];
|
|
1696
|
-
steps.forEach(s => {
|
|
1697
|
-
lines.push(`--- ${s.step} ---`);
|
|
1698
|
-
lines.push(`结果: ${s.result}`);
|
|
1699
|
-
if (s.raw) {
|
|
1700
|
-
lines.push(`详情: ${JSON.stringify(s.raw, null, 2)}`);
|
|
1701
|
-
}
|
|
1702
|
-
lines.push('');
|
|
1703
|
-
});
|
|
1704
|
-
lines.push('💡 诊断结论:');
|
|
1705
|
-
lines.push(` 输入 ID: ${input}`);
|
|
1706
|
-
lines.push(` 操作者: ${opId}`);
|
|
1707
|
-
lines.push('');
|
|
1708
|
-
lines.push(' 1️⃣ wiki/nodes API → ✅ 成功。节点存在,但无 document.docKey');
|
|
1709
|
-
lines.push(' 2️⃣ blocks API → ❌ "doc key is illegal"');
|
|
1710
|
-
lines.push(' 3️⃣ overwriteContent → ❌ 同上错误');
|
|
1711
|
-
lines.push(' 4️⃣ doc metadata → ❌ 404(该端点不存在)');
|
|
1712
|
-
lines.push('');
|
|
1713
|
-
lines.push('📌 DocContent API (GET /v2.0/doc/dentries/{uuid}/contents) ✅ 接受 dentryUuid');
|
|
1714
|
-
lines.push(' 上次失败原因: 503 ServiceUnavailable(临时服务器问题)');
|
|
1715
|
-
if (effectiveWsId) {
|
|
1716
|
-
lines.push(` 此文档 workspaceId: ${effectiveWsId}`);
|
|
1717
|
-
}
|
|
1718
|
-
lines.push('📌 请重新运行此工具,已加自动重试逻辑');
|
|
1719
|
-
|
|
1720
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
1310
|
case 'create_wiki_doc': {
|
|
1724
1311
|
const { workspace_id, parent_node_id, name, doc_type = 'DOC', operator_id } = args;
|
|
1725
1312
|
if (operator_id) {
|
|
@@ -2050,22 +1637,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2050
1637
|
let realDocKey = await resolveDocKey(docKey, operator_id || null, dingtalk);
|
|
2051
1638
|
console.error(`[DEBUG] get_wiki_doc_content: input=${docKey}, resolved=${realDocKey}`);
|
|
2052
1639
|
|
|
2053
|
-
//
|
|
2054
|
-
if (realDocKey === docKey && workspaceId) {
|
|
2055
|
-
try {
|
|
2056
|
-
const searchRes = await dingtalk.docRequest('GET', `/v1.0/doc/docs`, {
|
|
2057
|
-
operatorId: operator_id || null,
|
|
2058
|
-
extraParams: { workspaceId, keyword: '', maxResults: 50 }
|
|
2059
|
-
});
|
|
2060
|
-
const docs = searchRes.docs || [];
|
|
2061
|
-
const matched = docs.find(d => d.nodeBO?.nodeId === docKey || d.nodeBO?.nodeId === realDocKey) || docs[0];
|
|
2062
|
-
if (matched?.nodeBO?.nodeId) {
|
|
2063
|
-
realDocKey = matched.nodeBO.nodeId;
|
|
2064
|
-
console.error(`[DEBUG] get_wiki_doc_content: search fallback resolved to ${realDocKey}`);
|
|
2065
|
-
}
|
|
2066
|
-
} catch (e) { console.error(`[DEBUG] get_wiki_doc_content: search fallback failed: ${e.message}`); }
|
|
2067
|
-
}
|
|
2068
|
-
|
|
1640
|
+
// 尝试 blocks API 读取 .adoc 在线文档内容
|
|
2069
1641
|
try {
|
|
2070
1642
|
const result = await dingtalk.docRequest('GET', `/v1.0/doc/suites/documents/${realDocKey}/blocks`, { operatorId: operator_id || null });
|
|
2071
1643
|
const blocks = result.result?.data || [];
|
|
@@ -2085,9 +1657,76 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2085
1657
|
};
|
|
2086
1658
|
} catch (blocksErr) {
|
|
2087
1659
|
const isDocKeyIllegal = blocksErr.message.includes('doc key is illegal');
|
|
1660
|
+
|
|
1661
|
+
// Fallback: 尝试存储 v1.0 下载 API(对上传的 .md 等文件有效)
|
|
1662
|
+
if (isDocKeyIllegal && workspaceId) {
|
|
1663
|
+
try {
|
|
1664
|
+
const token = await dingtalk.getAccessToken();
|
|
1665
|
+
const opId = await dingtalk.resolveOperatorId(operator_id || null);
|
|
1666
|
+
|
|
1667
|
+
// BFS 遍历查找 dentry
|
|
1668
|
+
const queue = [''];
|
|
1669
|
+
const visited = new Set();
|
|
1670
|
+
let foundDentry = null;
|
|
1671
|
+
let foundSpaceId = null;
|
|
1672
|
+
while (queue.length > 0 && !foundDentry) {
|
|
1673
|
+
const parentId = queue.shift();
|
|
1674
|
+
if (visited.has(parentId)) continue;
|
|
1675
|
+
visited.add(parentId);
|
|
1676
|
+
try {
|
|
1677
|
+
const dirParams = { operatorId: opId, maxResults: 500 };
|
|
1678
|
+
if (parentId) dirParams.dentryId = parentId;
|
|
1679
|
+
const dirRes = await axios({
|
|
1680
|
+
method: 'GET',
|
|
1681
|
+
url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${workspaceId}/directories`,
|
|
1682
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1683
|
+
params: dirParams
|
|
1684
|
+
});
|
|
1685
|
+
const children = dirRes.data?.children || [];
|
|
1686
|
+
for (const child of children) {
|
|
1687
|
+
if (child.dentryUuid === realDocKey || child.dentryId === realDocKey || child.id === realDocKey) {
|
|
1688
|
+
foundDentry = child;
|
|
1689
|
+
foundSpaceId = child.spaceId;
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
if (child.hasChildren && child.dentryId && !visited.has(child.dentryId)) {
|
|
1693
|
+
queue.push(child.dentryId);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
} catch (e) { /* skip */ }
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
if (foundDentry && foundSpaceId) {
|
|
1700
|
+
const dlRes = await axios({
|
|
1701
|
+
method: 'POST',
|
|
1702
|
+
url: `${DINGTALK_API_V2}/v1.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}/downloadInfos/query`,
|
|
1703
|
+
headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
|
|
1704
|
+
params: { unionId: opId },
|
|
1705
|
+
data: {}
|
|
1706
|
+
});
|
|
1707
|
+
const data = dlRes.data;
|
|
1708
|
+
const downloadUrl = data.downloadInfo?.resourceUrl || data.resourceUrl || data.downloadUrl || data.url;
|
|
1709
|
+
const downloadHeaders = data.downloadInfo?.headers || data.headers || {};
|
|
1710
|
+
if (downloadUrl) {
|
|
1711
|
+
const contentRes = await axios({ method: 'GET', url: downloadUrl, headers: downloadHeaders, responseType: 'text' });
|
|
1712
|
+
return {
|
|
1713
|
+
content: [{ type: 'text', text: contentRes.data }]
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
} catch (fallbackErr) {
|
|
1718
|
+
console.error(`[钉钉MCP] 存储 v1.0 fallback 失败: ${fallbackErr.message}`);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
2088
1722
|
let hint = '';
|
|
2089
1723
|
if (isDocKeyIllegal) {
|
|
2090
|
-
hint = `\n\n原因:
|
|
1724
|
+
hint = `\n\n原因: 此文档不是 .adoc 在线文档,blocks API 无法读取。`;
|
|
1725
|
+
if (workspaceId) {
|
|
1726
|
+
hint += `\n已尝试存储 v1.0 下载 API 但仍失败(需开通 Storage.DownloadInfo.Read 权限)。`;
|
|
1727
|
+
} else {
|
|
1728
|
+
hint += `\n请提供 workspace_id 以尝试存储 v1.0 下载 API。`;
|
|
1729
|
+
}
|
|
2091
1730
|
}
|
|
2092
1731
|
return {
|
|
2093
1732
|
content: [{
|
|
@@ -2107,20 +1746,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2107
1746
|
|
|
2108
1747
|
let realDocKey = await resolveDocKey(docKey, operator_id || null, dingtalk);
|
|
2109
1748
|
|
|
2110
|
-
|
|
2111
|
-
try {
|
|
2112
|
-
const searchRes = await dingtalk.docRequest('GET', `/v1.0/doc/docs`, {
|
|
2113
|
-
operatorId: operator_id || null,
|
|
2114
|
-
extraParams: { workspaceId, keyword: '', maxResults: 50 }
|
|
2115
|
-
});
|
|
2116
|
-
const docs = searchRes.docs || [];
|
|
2117
|
-
const matched = docs.find(d => d.nodeBO?.nodeId === docKey || d.nodeBO?.nodeId === realDocKey) || docs[0];
|
|
2118
|
-
if (matched?.nodeBO?.nodeId) {
|
|
2119
|
-
realDocKey = matched.nodeBO.nodeId;
|
|
2120
|
-
}
|
|
2121
|
-
} catch (e) { /* ignore */ }
|
|
2122
|
-
}
|
|
2123
|
-
|
|
1749
|
+
// 尝试 overwriteContent API 写入 .adoc 在线文档内容
|
|
2124
1750
|
try {
|
|
2125
1751
|
await dingtalk.docRequest('POST', `/v1.0/doc/suites/documents/${realDocKey}/overwriteContent`, {
|
|
2126
1752
|
operatorId: operator_id || null,
|
|
@@ -2130,7 +1756,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2130
1756
|
const isDocKeyIllegal = e.message.includes('doc key is illegal');
|
|
2131
1757
|
let hint = '';
|
|
2132
1758
|
if (isDocKeyIllegal) {
|
|
2133
|
-
hint = `\n\n原因:
|
|
1759
|
+
hint = `\n\n原因: 此文档不是 .adoc 在线文档(可能是上传的 .md 文件),不支持修改。overwriteContent 仅对 .adoc 在线文档有效。`;
|
|
2134
1760
|
}
|
|
2135
1761
|
return {
|
|
2136
1762
|
content: [{ type: 'text', text: `❌ 写入文档内容失败\n${e.message}${hint}` }],
|