dingtalk-wiki 1.2.16 → 1.2.18

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 +30 -472
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -223,7 +223,7 @@ async function rebuildSearchIndex() {
223
223
  method: 'POST',
224
224
  url: `${DINGTALK_API_V2}/v1.0/storage/spaces/${child.spaceId}/dentries/${child.dentryId}/downloadInfos/query`,
225
225
  headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
226
- params: { unionId: await dingtalk.getCurrentUserUnionId() || '' },
226
+ params: { unionId: await dingtalk.resolveOperatorId(null) },
227
227
  data: {}
228
228
  });
229
229
  const data = dlRes.data;
@@ -747,28 +747,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
747
747
  required: ['node_id']
748
748
  }
749
749
  },
750
- {
751
- name: 'debug_resolve_doc_key',
752
- description: '[调试] 诊断 docKey 解析问题,测试所有可能的 API 路径并返回原始响应',
753
- inputSchema: {
754
- type: 'object',
755
- properties: {
756
- doc_key: {
757
- type: 'string',
758
- description: '文档标识(dentryUuid / docKey / nodeId / URL)'
759
- },
760
- workspace_id: {
761
- type: 'string',
762
- description: '知识库 ID(可选)'
763
- },
764
- operator_id: {
765
- type: 'string',
766
- description: '操作者 unionid(不传则使用默认用户)'
767
- }
768
- },
769
- required: ['doc_key']
770
- }
771
- },
772
750
  {
773
751
  name: 'search_wiki',
774
752
  description: '按名称搜索知识库中的文档和文件夹(遍历目录树,无需索引即可使用)',
@@ -893,17 +871,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
893
871
  },
894
872
  {
895
873
  name: 'get_wiki_doc_content',
896
- description: '读取文档正文内容(返回 Block 结构)。注意:仅对 create_wiki_doc 创建的文档有效;知识库中已有文档(非通过此工具创建的)不受支持——钉钉未提供公开 REST API 读取已有知识库文档内容。若需读取已有文档内容,可使用 DingTalk 官方 MCP 服务器的 get_document_content 工具。',
874
+ description: '读取文档内容。对 .adoc 在线文档使用 blocks API;对上传的 .md 等文件使用存储下载 API(需 Storage.DownloadInfo.Read 权限)。',
897
875
  inputSchema: {
898
876
  type: 'object',
899
877
  properties: {
900
878
  doc_key: {
901
879
  type: 'string',
902
- description: '文档标识。支持 wiki nodes 的 nodeId(dentryUuid)、docKey,或文档 URL。重要:create_wiki_doc 返回的 nodeId 不能用于内容读写,请使用 docKey 或 dentryUuid。若传入 create 的 nodeId,需额外提供 workspace_id 以自动查找'
880
+ description: '文档标识。支持 nodeId(dentryUuid)、docKey,或文档 URL'
903
881
  },
904
882
  workspace_id: {
905
883
  type: 'string',
906
- description: '知识库 ID(可选)。当 doc_key 来自 create_wiki_doc 返回的 nodeId 时,提供此参数可自动查找正确的 dentryUuid'
884
+ description: '知识库 ID(可选)。自动通过 wiki/nodes API 解析。'
907
885
  },
908
886
  operator_id: {
909
887
  type: 'string',
@@ -915,13 +893,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
915
893
  },
916
894
  {
917
895
  name: 'update_wiki_doc_content',
918
- description: '覆写文档正文内容(⚠️ 全量覆盖,不可撤销)。注意:仅对 create_wiki_doc 创建的文档有效;知识库中已有文档(非通过此工具创建的)不受支持。',
896
+ description: '覆写 .adoc 在线文档正文内容(⚠️ 全量覆盖,不可撤销)。对上传的 .md 等文件不支持修改。',
919
897
  inputSchema: {
920
898
  type: 'object',
921
899
  properties: {
922
900
  doc_key: {
923
901
  type: 'string',
924
- description: '文档标识。支持 wiki nodes 的 nodeId(dentryUuid)、docKey,或文档 URL。重要:create_wiki_doc 返回的 nodeId 不能用于内容读写,请使用 docKey 或 dentryUuid。若传入 create 的 nodeId,需额外提供 workspace_id 以自动查找'
902
+ description: '文档标识。支持 nodeId(dentryUuid)、docKey,或文档 URL。仅 .adoc 在线文档支持写入。'
925
903
  },
926
904
  content: {
927
905
  type: 'string',
@@ -1214,6 +1192,11 @@ async function resolveDocKey(docKey, operatorId, dingtalkInstance) {
1214
1192
  const node = nodeInfo.node || nodeInfo;
1215
1193
  console.error(`[DEBUG] resolveDocKey: wiki/nodes response keys=${Object.keys(node).join(',')}`);
1216
1194
 
1195
+ // 缓存 workspaceId,供后续 fallback 使用
1196
+ if (node.workspaceId) {
1197
+ dingtalkInstance._resolvedWorkspaceId = node.workspaceId;
1198
+ }
1199
+
1217
1200
  // wiki/nodes 的 nodeId = dentryUuid,直接可用
1218
1201
  // 但 node.document.docKey 可能是不同的值,优先使用
1219
1202
  if (node.document?.docKey) {
@@ -1329,410 +1312,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1329
1312
  };
1330
1313
  }
1331
1314
 
1332
- case 'debug_resolve_doc_key': {
1333
- const { doc_key: docKey, workspace_id: workspaceId, operator_id } = args;
1334
- if (operator_id) {
1335
- dingtalk.setOperatorId(operator_id);
1336
- }
1337
-
1338
- const token = await dingtalk.getAccessToken();
1339
- const opId = await dingtalk.resolveOperatorId(operator_id || null);
1340
- const steps = [];
1341
- const input = String(docKey);
1342
-
1343
- steps.push({ step: '输入', result: input });
1344
-
1345
- // Step 1: URL extraction
1346
- const urlMatch = input.match(/\/i\/nodes\/([^\/\?#]+)/);
1347
- const urlExtracted = urlMatch ? urlMatch[1] : null;
1348
- steps.push({ step: 'URL 提取', result: urlExtracted || '无匹配', raw: urlMatch ? urlMatch[0] : null });
1349
-
1350
- // Step 2: wiki/nodes API
1351
- let wikiNodeResponse = null;
1352
- let wikiWsId = null;
1353
- try {
1354
- const nodeRes = await axios({
1355
- method: 'GET',
1356
- url: `${DINGTALK_API_V2}/v2.0/wiki/nodes/${input}`,
1357
- headers: { 'x-acs-dingtalk-access-token': token },
1358
- params: { operatorId: opId }
1359
- });
1360
- wikiNodeResponse = nodeRes.data;
1361
- wikiWsId = wikiNodeResponse.workspaceId || wikiNodeResponse.node?.workspaceId || null;
1362
- const node = wikiNodeResponse;
1363
- steps.push({
1364
- step: 'wiki/nodes API',
1365
- result: '成功',
1366
- raw: node
1367
- });
1368
- } catch (e) {
1369
- steps.push({
1370
- step: 'wiki/nodes API',
1371
- result: '失败',
1372
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1373
- });
1374
- }
1375
-
1376
- const effectiveWsId = workspaceId || wikiWsId;
1377
-
1378
- // Step 3: doc metadata API (GET /v1.0/doc/suites/documents/{docKey})
1379
- try {
1380
- const metaRes = await axios({
1381
- method: 'GET',
1382
- url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}`,
1383
- headers: { 'x-acs-dingtalk-access-token': token },
1384
- params: { operatorId: opId }
1385
- });
1386
- steps.push({
1387
- step: 'doc metadata API (GET /documents/{docKey})',
1388
- result: '成功',
1389
- raw: metaRes.data
1390
- });
1391
- } catch (e) {
1392
- steps.push({
1393
- step: 'doc metadata API (GET /documents/{docKey})',
1394
- result: '失败',
1395
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1396
- });
1397
- }
1398
-
1399
- // Step 4: blocks API
1400
- try {
1401
- const blocksRes = await axios({
1402
- method: 'GET',
1403
- url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}/blocks`,
1404
- headers: { 'x-acs-dingtalk-access-token': token },
1405
- params: { operatorId: opId }
1406
- });
1407
- const blockCount = blocksRes.data?.result?.data?.length || 0;
1408
- steps.push({
1409
- step: 'blocks API',
1410
- result: `成功 (${blockCount} 个 block)`,
1411
- raw: blocksRes.data
1412
- });
1413
- } catch (e) {
1414
- steps.push({
1415
- step: 'blocks API',
1416
- result: '失败',
1417
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1418
- });
1419
- }
1420
-
1421
- // Step 5: overwriteContent API (test with empty content — don't actually write)
1422
- try {
1423
- const overwriteRes = await axios({
1424
- method: 'POST',
1425
- url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}/overwriteContent`,
1426
- headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
1427
- params: { operatorId: opId },
1428
- data: { content: 'test', contentType: 'markdown' }
1429
- });
1430
- steps.push({
1431
- step: 'overwriteContent API',
1432
- result: '成功(可写入)',
1433
- raw: overwriteRes.data
1434
- });
1435
- } catch (e) {
1436
- steps.push({
1437
- step: 'overwriteContent API',
1438
- result: '失败',
1439
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1440
- });
1441
- }
1442
-
1443
- // Step 6: search docs API (v1.0/doc/docs) — 需要 workspaceId + keyword
1444
- if (effectiveWsId) {
1445
- // Try without keyword first (might list all)
1446
- try {
1447
- const searchRes = await axios({
1448
- method: 'GET',
1449
- url: `${DINGTALK_API_V2}/v1.0/doc/docs`,
1450
- headers: { 'x-acs-dingtalk-access-token': token },
1451
- params: { operatorId: opId, workspaceId: effectiveWsId, maxResults: 50 }
1452
- });
1453
- const docs = searchRes.data?.docs || [];
1454
- const matched = docs.find(d => d.nodeBO?.nodeId === input || d.nodeBO?.dentryUuid === input || d.docKey === input || d.nodeBO?.dentryId === input);
1455
- steps.push({
1456
- step: `doc search API (workspace=${effectiveWsId}, no keyword)`,
1457
- result: `成功 (${docs.length} 篇文档)` + (matched ? ', 找到匹配!' : ', 未找到匹配'),
1458
- raw: docs.length > 0 ? { count: docs.length, firstDocDocKey: docs[0]?.docKey || '(无)', firstDocNodeId: docs[0]?.nodeBO?.nodeId || '(无)', firstDocName: docs[0]?.name || '(无)' } : '空结果'
1459
- });
1460
- } catch (e) {
1461
- steps.push({
1462
- step: `doc search API (workspace=${effectiveWsId}, no keyword)`,
1463
- result: '失败',
1464
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1465
- });
1466
- }
1467
- // Try with keyword from wiki/nodes name if available
1468
- const wikiName = wikiNodeResponse?.node?.name || wikiNodeResponse?.name || '';
1469
- if (wikiName) {
1470
- try {
1471
- const searchRes = await axios({
1472
- method: 'GET',
1473
- url: `${DINGTALK_API_V2}/v1.0/doc/docs`,
1474
- headers: { 'x-acs-dingtalk-access-token': token },
1475
- params: { operatorId: opId, workspaceId: effectiveWsId, keyword: wikiName.replace(/\.\w+$/, ''), maxResults: 10 }
1476
- });
1477
- const docs = searchRes.data?.docs || [];
1478
- steps.push({
1479
- step: `doc search API (keyword="${wikiName}")`,
1480
- result: `成功 (${docs.length} 篇)`,
1481
- raw: docs.length > 0 ? docs.map(d => ({ docKey: d.docKey || '(无)', name: d.name || '(无)', nodeId: d.nodeBO?.nodeId || '(无)', dentryUuid: d.nodeBO?.dentryUuid || '(无)' })) : '空结果'
1482
- });
1483
- } catch (e) {
1484
- steps.push({
1485
- step: `doc search API (keyword="${wikiName}")`,
1486
- result: '失败',
1487
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1488
- });
1489
- }
1490
- }
1491
- } else {
1492
- steps.push({ step: 'doc search API', result: '跳过(无法获取 workspace_id)' });
1493
- }
1494
-
1495
- // Step 7: 在知识库目录中查找此节点 + 查看是否有 docKey 字段
1496
- if (effectiveWsId) {
1497
- try {
1498
- const dirRes = await axios({
1499
- method: 'GET',
1500
- url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${effectiveWsId}/directories`,
1501
- headers: { 'x-acs-dingtalk-access-token': token },
1502
- params: { operatorId: opId, maxResults: 500 }
1503
- });
1504
- const children = dirRes.data?.children || [];
1505
- const match = children.find(c => c.dentryUuid === input || c.dentryId === input || c.id === input);
1506
- // Show first few children's available fields so user can see what fields exist
1507
- const sampleKeys = children.length > 0 ? Object.keys(children[0]) : [];
1508
- steps.push({
1509
- step: `知识库目录 (workspace=${effectiveWsId})`,
1510
- result: `${children.length} 个子节点` + (match ? ', 找到匹配!' : ', 未找到匹配'),
1511
- raw: match ? match : { sampleFields: sampleKeys, firstChildSample: children[0] || null }
1512
- });
1513
- } catch (e) {
1514
- steps.push({
1515
- step: `知识库目录 (workspace=${effectiveWsId})`,
1516
- result: '失败',
1517
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1518
- });
1519
- }
1520
- }
1521
-
1522
- // Step 8: 用 workspaceId 再次尝试 overwriteContent(确认文档套件是否完全不可用)
1523
- if (effectiveWsId) {
1524
- try {
1525
- const overwriteRes = await axios({
1526
- method: 'POST',
1527
- url: `${DINGTALK_API_V2}/v1.0/doc/suites/documents/${input}/overwriteContent`,
1528
- headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
1529
- params: { operatorId: opId },
1530
- data: { content: 'test', contentType: 'markdown' }
1531
- });
1532
- steps.push({
1533
- step: 'overwriteContent(带 workspaceId)',
1534
- result: '成功',
1535
- raw: overwriteRes.data
1536
- });
1537
- } catch (e) {
1538
- steps.push({
1539
- step: 'overwriteContent(带 workspaceId)',
1540
- result: '失败',
1541
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1542
- });
1543
- }
1544
- }
1545
-
1546
- // Step 9: 递归遍历目录树查找文档 + 存储API下载内容
1547
- let foundDentry = null;
1548
- let foundSpaceId = null;
1549
- if (effectiveWsId) {
1550
- try {
1551
- // BFS 遍历目录树查找 dentry
1552
- const queue = [''];
1553
- const visited = new Set();
1554
- while (queue.length > 0 && !foundDentry) {
1555
- const parentId = queue.shift();
1556
- if (visited.has(parentId)) continue;
1557
- visited.add(parentId);
1558
- try {
1559
- const dirParams = { operatorId: opId, maxResults: 500 };
1560
- if (parentId) dirParams.dentryId = parentId;
1561
- const dirRes = await axios({
1562
- method: 'GET',
1563
- url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${effectiveWsId}/directories`,
1564
- headers: { 'x-acs-dingtalk-access-token': token },
1565
- params: dirParams
1566
- });
1567
- const children = dirRes.data?.children || [];
1568
- for (const child of children) {
1569
- if (child.dentryUuid === input || child.dentryId === input) {
1570
- foundDentry = child;
1571
- foundSpaceId = child.spaceId;
1572
- break;
1573
- }
1574
- if (child.hasChildren && child.dentryId && !visited.has(child.dentryId)) {
1575
- queue.push(child.dentryId);
1576
- }
1577
- }
1578
- } catch (e) { /* skip */ }
1579
- }
1580
-
1581
- if (foundDentry) {
1582
- steps.push({
1583
- step: 'BFS 目录遍历找到文档',
1584
- result: `spaceId=${foundSpaceId}, dentryId=${foundDentry.dentryId}, name=${foundDentry.name}`,
1585
- raw: foundDentry
1586
- });
1587
-
1588
- // 尝试存储 API 获取 dentry 详情(含 docKey)
1589
- try {
1590
- const dentryRes = await axios({
1591
- method: 'GET',
1592
- url: `${DINGTALK_API_V2}/v2.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}`,
1593
- headers: { 'x-acs-dingtalk-access-token': token },
1594
- params: { operatorId: opId }
1595
- });
1596
- steps.push({
1597
- step: '存储 API dentry 详情',
1598
- result: '成功',
1599
- raw: dentryRes.data
1600
- });
1601
- } catch (e) {
1602
- steps.push({
1603
- step: '存储 API dentry 详情',
1604
- result: '失败',
1605
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1606
- });
1607
- }
1608
-
1609
- // 尝试存储 API 下载
1610
- try {
1611
- const dlRes = await axios({
1612
- method: 'POST',
1613
- url: `${DINGTALK_API_V2}/v2.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}/downloadInfos/query`,
1614
- headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
1615
- params: { operatorId: opId },
1616
- data: {}
1617
- });
1618
- steps.push({
1619
- step: '存储 API 获取下载链接',
1620
- result: '成功',
1621
- raw: dlRes.data
1622
- });
1623
- // 尝试从下载链接获取内容
1624
- const dlInfo = dlRes.data;
1625
- const downloadUrl = dlInfo.headerSignatureInfo?.resourceUrls?.[0] || dlInfo.downloadUrl || dlInfo.url;
1626
- if (downloadUrl) {
1627
- try {
1628
- const contentRes = await axios({ method: 'GET', url: downloadUrl, responseType: 'text' });
1629
- steps.push({
1630
- step: '存储 API 下载内容',
1631
- result: `成功 (${contentRes.data.length} 字符)`,
1632
- raw: contentRes.data.slice(0, 500) + (contentRes.data.length > 500 ? '\n... (截断)' : '')
1633
- });
1634
- } catch (dlErr) {
1635
- steps.push({
1636
- step: '存储 API 下载内容',
1637
- result: '失败',
1638
- raw: dlErr.message
1639
- });
1640
- }
1641
- }
1642
- } catch (e) {
1643
- steps.push({
1644
- step: '存储 API 获取下载链接',
1645
- result: '失败',
1646
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1647
- });
1648
- }
1649
- } else {
1650
- steps.push({ step: 'BFS 目录遍历', result: '未找到匹配(文档可能在深层目录)' });
1651
- }
1652
- } catch (e) {
1653
- steps.push({ step: 'BFS 目录遍历', result: '失败', raw: e.message });
1654
- }
1655
- }
1656
-
1657
- // Step 10: 存储 API v1.0 - 文件下载(.md 文件内容 = 文件下载)
1658
- if (foundSpaceId) {
1659
- try {
1660
- const dlRes = await axios({
1661
- method: 'POST',
1662
- url: `${DINGTALK_API_V2}/v1.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}/downloadInfos/query`,
1663
- headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
1664
- params: { unionId: opId },
1665
- data: {}
1666
- });
1667
- const data = dlRes.data;
1668
- steps.push({
1669
- step: '存储 v1.0 获取下载链接 (POST /v1.0/storage/spaces/{sid}/dentries/{did}/downloadInfos/query)',
1670
- result: '成功',
1671
- raw: data
1672
- });
1673
- // 如果有下载链接,尝试下载内容
1674
- const downloadUrl = data.downloadInfo?.resourceUrl || data.resourceUrl || data.downloadUrl || data.url;
1675
- const downloadHeaders = data.downloadInfo?.headers || data.headers || {};
1676
- if (downloadUrl) {
1677
- try {
1678
- const contentRes = await axios({ method: 'GET', url: downloadUrl, headers: downloadHeaders, responseType: 'text' });
1679
- steps.push({
1680
- step: '文件内容下载',
1681
- result: `成功 (${contentRes.data.length} 字符)`,
1682
- raw: contentRes.data.slice(0, 1000) + (contentRes.data.length > 1000 ? '\n...(截断)' : '')
1683
- });
1684
- } catch (dlErr) {
1685
- steps.push({ step: '文件内容下载', result: '失败', raw: dlErr.message });
1686
- }
1687
- }
1688
- } catch (e) {
1689
- steps.push({
1690
- step: '存储 v1.0 获取下载链接',
1691
- result: '失败',
1692
- raw: { message: e.response?.data?.message || e.message, code: e.response?.data?.code || '(无)', status: e.response?.status || '(无)' }
1693
- });
1694
- }
1695
- }
1696
-
1697
-
1698
-
1699
- const lines = ['🔍 docKey 诊断报告', '', `输入: ${input}`, `operatorId: ${opId}`, ''];
1700
- steps.forEach(s => {
1701
- lines.push(`--- ${s.step} ---`);
1702
- lines.push(`结果: ${s.result}`);
1703
- if (s.raw) {
1704
- lines.push(`详情: ${JSON.stringify(s.raw, null, 2)}`);
1705
- }
1706
- lines.push('');
1707
- });
1708
- const isAdoc = foundDentry?.name?.endsWith('.adoc') || input.length > 20;
1709
- lines.push('💡 诊断结论:');
1710
- lines.push(` 输入 ID: ${input}`);
1711
- lines.push(` 操作者: ${opId}`);
1712
- lines.push('');
1713
- if (foundDentry) {
1714
- lines.push(` 文档名: ${foundDentry.name}`);
1715
- lines.push(` 类型推断: ${foundDentry.name?.endsWith('.adoc') ? '✅ .adoc 在线文档(可读写)' : foundDentry.name?.match(/\.(md|docx|xlsx|txt|xmind|mark|markdown)$/i) ? '❌ 导入的文档(非原生. adoc,内容 API 不支持)' : '⚠️ 未知类型'}`);
1716
- }
1717
- lines.push('');
1718
- lines.push(' 1️⃣ wiki/nodes API → ✅ 成功。节点存在');
1719
- lines.push(` 2️⃣ blocks API → ${foundDentry?.name?.endsWith('.adoc') ? '✅ 应对 .adoc 在线文档有效' : '❌ "doc key is illegal"(非 .adoc 文档无 docKey)'}`);
1720
- lines.push(` 3️⃣ overwriteContent → ${foundDentry?.name?.endsWith('.adoc') ? '✅ 应对 .adoc 在线文档有效' : '❌ 同上错误'}`);
1721
- lines.push(' 4️⃣ doc metadata → ❌ 404(该端点不存在)');
1722
- lines.push('');
1723
- lines.push('📌 关键区别:');
1724
- lines.push(' - 知识库"导入为在线文档" → 转为 .adoc → 有 docKey → blocks API 可读写');
1725
- lines.push(' - "上传文件" → 保持原格式 → 无 docKey → blocks API 不可用');
1726
- lines.push(' - create_wiki_doc 创建的是 .adoc 在线文档 → blocks API 可读写');
1727
- lines.push('');
1728
- lines.push('📌 存储 v1.0 下载 API (POST /v1.0/storage/spaces/{sid}/dentries/{did}/downloadInfos/query)');
1729
- lines.push(' 使用 unionId 参数,可获取上传文件的下载链接');
1730
- lines.push('📌 DocContent API (GET /v2.0/doc/dentries/{uuid}/contents) 此前 503');
1731
- lines.push('📌 请重新运行此工具测试以上 API');
1732
-
1733
- return { content: [{ type: 'text', text: lines.join('\n') }] };
1734
- }
1735
-
1736
1315
  case 'create_wiki_doc': {
1737
1316
  const { workspace_id, parent_node_id, name, doc_type = 'DOC', operator_id } = args;
1738
1317
  if (operator_id) {
@@ -1771,6 +1350,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1771
1350
  });
1772
1351
 
1773
1352
  const doc = response.data;
1353
+ const nodeId = doc.nodeId || doc.id;
1774
1354
  const typeLabels = {
1775
1355
  DOC: '文档',
1776
1356
  WORKBOOK: '表格',
@@ -1790,7 +1370,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1790
1370
  '',
1791
1371
  `${typeIcon} ${name}`,
1792
1372
  `🗂️ 类型: ${doc_type}`,
1793
- `🆔 Node ID: ${doc.nodeId}`,
1373
+ `🆔 Node ID: ${nodeId}`,
1794
1374
  ];
1795
1375
 
1796
1376
  if (doc.docKey) {
@@ -1807,7 +1387,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1807
1387
  }
1808
1388
 
1809
1389
  setImmediate(() => {
1810
- wikiIndex.add(doc.nodeId || doc.docKey, {
1390
+ wikiIndex.add(nodeId || doc.docKey, {
1811
1391
  title: name,
1812
1392
  content: args.content || '',
1813
1393
  workspaceId: workspace_id || doc.workspaceId,
@@ -1815,7 +1395,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1815
1395
  url: doc.url || '',
1816
1396
  type: doc_type,
1817
1397
  });
1818
- const contentId = doc.docKey || doc.dentryUuid || doc.nodeId;
1398
+ const contentId = doc.docKey || doc.dentryUuid || nodeId;
1819
1399
  if (args.content && contentId) {
1820
1400
  dingtalk.docRequest('POST', `/v1.0/doc/suites/documents/${contentId}/overwriteContent`, {
1821
1401
  operatorId: opId,
@@ -1870,7 +1450,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1870
1450
  const result = await dingtalk.docRequest('GET', `/v2.0/wiki/nodes/${node_id}`, {
1871
1451
  operatorId: operator_id || null
1872
1452
  });
1873
- const node = result;
1453
+ const node = result.node || result;
1874
1454
  let output = `📄 节点详情\n\n`;
1875
1455
  output += `名称: ${node.name || '-'}\n`;
1876
1456
  output += `ID: ${node.id || node.nodeId || '-'}\n`;
@@ -1932,7 +1512,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1932
1512
  for (const child of children) {
1933
1513
  const name = child.name || '';
1934
1514
  if (name.includes(keyword)) {
1935
- matchedNodes.push({ name, nodeId: child.dentryId || child.nodeId || child.id, workspaceId: ws.workspaceId, workspaceName: ws.name, type: child.contentType === 'folder' ? '文件夹' : '文档', url: child.url || '' });
1515
+ matchedNodes.push({ name, nodeId: child.dentryUuid || child.dentryId || child.nodeId || child.id, workspaceId: ws.workspaceId, workspaceName: ws.name, type: child.contentType === 'folder' ? '文件夹' : '文档', url: child.url || '' });
1936
1516
  }
1937
1517
  const childId = child.dentryId || child.nodeId || child.id;
1938
1518
  if (child.hasChildren && childId && !visited.has(childId)) queue.push(childId);
@@ -1988,6 +1568,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1988
1568
  const icon = item.type === 'FOLDER' || item.type === 'folder' ? '📁' : '📄';
1989
1569
  output += `${i + 1}. ${icon} ${item.title}\n`;
1990
1570
  output += ` 知识库: ${item.workspaceName} (${item.workspaceId})\n`;
1571
+ output += ` Node ID: ${item.id}\n`;
1991
1572
  output += ` 匹配度: ${(item.score * 100).toFixed(0)}%\n`;
1992
1573
  if (item.url) output += ` 链接: ${item.url}\n`;
1993
1574
  if (item.content) {
@@ -2059,26 +1640,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2059
1640
  if (operator_id) {
2060
1641
  dingtalk.setOperatorId(operator_id);
2061
1642
  }
1643
+ dingtalk._resolvedWorkspaceId = null;
2062
1644
 
2063
1645
  let realDocKey = await resolveDocKey(docKey, operator_id || null, dingtalk);
2064
1646
  console.error(`[DEBUG] get_wiki_doc_content: input=${docKey}, resolved=${realDocKey}`);
2065
1647
 
2066
- // 如果 resolve 后值没变(可能是 create 返回的 nodeId),尝试搜索 API 查找
2067
- if (realDocKey === docKey && workspaceId) {
2068
- try {
2069
- const searchRes = await dingtalk.docRequest('GET', `/v1.0/doc/docs`, {
2070
- operatorId: operator_id || null,
2071
- extraParams: { workspaceId, keyword: '', maxResults: 50 }
2072
- });
2073
- const docs = searchRes.docs || [];
2074
- const matched = docs.find(d => d.nodeBO?.nodeId === docKey || d.nodeBO?.nodeId === realDocKey) || docs[0];
2075
- if (matched?.nodeBO?.nodeId) {
2076
- realDocKey = matched.nodeBO.nodeId;
2077
- console.error(`[DEBUG] get_wiki_doc_content: search fallback resolved to ${realDocKey}`);
2078
- }
2079
- } catch (e) { console.error(`[DEBUG] get_wiki_doc_content: search fallback failed: ${e.message}`); }
2080
- }
1648
+ const effectiveWsId = workspaceId || dingtalk._resolvedWorkspaceId;
2081
1649
 
1650
+ // 尝试 blocks API 读取 .adoc 在线文档内容
2082
1651
  try {
2083
1652
  const result = await dingtalk.docRequest('GET', `/v1.0/doc/suites/documents/${realDocKey}/blocks`, { operatorId: operator_id || null });
2084
1653
  const blocks = result.result?.data || [];
@@ -2100,7 +1669,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2100
1669
  const isDocKeyIllegal = blocksErr.message.includes('doc key is illegal');
2101
1670
 
2102
1671
  // Fallback: 尝试存储 v1.0 下载 API(对上传的 .md 等文件有效)
2103
- if (isDocKeyIllegal && workspaceId) {
1672
+ if (isDocKeyIllegal && effectiveWsId) {
2104
1673
  try {
2105
1674
  const token = await dingtalk.getAccessToken();
2106
1675
  const opId = await dingtalk.resolveOperatorId(operator_id || null);
@@ -2119,7 +1688,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2119
1688
  if (parentId) dirParams.dentryId = parentId;
2120
1689
  const dirRes = await axios({
2121
1690
  method: 'GET',
2122
- url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${workspaceId}/directories`,
1691
+ url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${effectiveWsId}/directories`,
2123
1692
  headers: { 'x-acs-dingtalk-access-token': token },
2124
1693
  params: dirParams
2125
1694
  });
@@ -2163,14 +1732,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2163
1732
  let hint = '';
2164
1733
  if (isDocKeyIllegal) {
2165
1734
  hint = `\n\n原因: 此文档不是 .adoc 在线文档,blocks API 无法读取。`;
2166
- if (workspaceId) {
2167
- hint += `\n已尝试存储 v1.0 下载 API 但仍失败。`;
1735
+ if (effectiveWsId) {
1736
+ hint += `\n已尝试存储 v1.0 下载 API 但仍失败(需开通 Storage.DownloadInfo.Read 权限)。`;
1737
+ } else {
1738
+ hint += `\n请提供 workspace_id 以尝试存储 v1.0 下载 API。`;
2168
1739
  }
2169
1740
  }
2170
1741
  return {
2171
1742
  content: [{
2172
1743
  type: 'text',
2173
- text: `❌ 读取文档内容失败\n错误: ${blocksErr.message}${hint}\n\n调试信息:\n- 输入 doc_key: ${docKey}\n- 解析后 realDocKey: ${realDocKey}\n- workspaceId: ${workspaceId || '未提供'}\n- operator_id: ${operator_id || '未提供(将自动解析)'}`
1744
+ text: `❌ 读取文档内容失败\n错误: ${blocksErr.message}${hint}\n\n调试信息:\n- 输入 doc_key: ${docKey}\n- 解析后 realDocKey: ${realDocKey}\n- workspaceId: ${effectiveWsId || '未提供'}\n- operator_id: ${operator_id || '未提供(将自动解析)'}`
2174
1745
  }],
2175
1746
  isError: true
2176
1747
  };
@@ -2185,20 +1756,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2185
1756
 
2186
1757
  let realDocKey = await resolveDocKey(docKey, operator_id || null, dingtalk);
2187
1758
 
2188
- if (realDocKey === docKey && workspaceId) {
2189
- try {
2190
- const searchRes = await dingtalk.docRequest('GET', `/v1.0/doc/docs`, {
2191
- operatorId: operator_id || null,
2192
- extraParams: { workspaceId, keyword: '', maxResults: 50 }
2193
- });
2194
- const docs = searchRes.docs || [];
2195
- const matched = docs.find(d => d.nodeBO?.nodeId === docKey || d.nodeBO?.nodeId === realDocKey) || docs[0];
2196
- if (matched?.nodeBO?.nodeId) {
2197
- realDocKey = matched.nodeBO.nodeId;
2198
- }
2199
- } catch (e) { /* ignore */ }
2200
- }
2201
-
1759
+ // 尝试 overwriteContent API 写入 .adoc 在线文档内容
2202
1760
  try {
2203
1761
  await dingtalk.docRequest('POST', `/v1.0/doc/suites/documents/${realDocKey}/overwriteContent`, {
2204
1762
  operatorId: operator_id || null,
@@ -2208,7 +1766,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2208
1766
  const isDocKeyIllegal = e.message.includes('doc key is illegal');
2209
1767
  let hint = '';
2210
1768
  if (isDocKeyIllegal) {
2211
- hint = `\n\n原因: 此文档在 Doc Suite API 中无关联 docKey。overwriteContent 仅对通过 create_wiki_doc 创建的文档有效。`;
1769
+ hint = `\n\n原因: 此文档不是 .adoc 在线文档(可能是上传的 .md 文件),不支持修改。overwriteContent 仅对 .adoc 在线文档有效。`;
2212
1770
  }
2213
1771
  return {
2214
1772
  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.16",
3
+ "version": "1.2.18",
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": {