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.
Files changed (2) hide show
  1. package/index.js +93 -467
  2. 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
- // 如果 resolve 后值没变(可能是 create 返回的 nodeId),尝试搜索 API 查找
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原因: 此文档的 dentryUuid 在 Doc Suite API 中无关联 docKey。blocks/overwriteContent\n仅对通过 create_wiki_doc 创建的文档有效。对于知识库中已有文档,钉钉未提供公\n开 REST API 读取内容。可通过 DingTalk 官方 MCP 服务器读取(get_document_content)。`;
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
- if (realDocKey === docKey && workspaceId) {
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原因: 此文档在 Doc Suite API 中无关联 docKey。overwriteContent 仅对通过 create_wiki_doc 创建的文档有效。`;
1759
+ hint = `\n\n原因: 此文档不是 .adoc 在线文档(可能是上传的 .md 文件),不支持修改。overwriteContent 仅对 .adoc 在线文档有效。`;
2134
1760
  }
2135
1761
  return {
2136
1762
  content: [{ type: 'text', text: `❌ 写入文档内容失败\n${e.message}${hint}` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-wiki",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
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": {