dingtalk-wiki 1.2.15 → 1.2.16

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 +141 -63
  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.getCurrentUserUnionId() || '' },
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 {
@@ -1633,60 +1654,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1633
1654
  }
1634
1655
  }
1635
1656
 
1636
- // Step 10: 存储 API - 文件下载(.md 文件内容 = 文件下载)
1657
+ // Step 10: 存储 API v1.0 - 文件下载(.md 文件内容 = 文件下载)
1637
1658
  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
- });
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
+ }
1689
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
+ });
1690
1694
  }
1691
1695
  }
1692
1696
 
@@ -1701,21 +1705,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1701
1705
  }
1702
1706
  lines.push('');
1703
1707
  });
1708
+ const isAdoc = foundDentry?.name?.endsWith('.adoc') || input.length > 20;
1704
1709
  lines.push('💡 诊断结论:');
1705
1710
  lines.push(` 输入 ID: ${input}`);
1706
1711
  lines.push(` 操作者: ${opId}`);
1707
1712
  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 同上错误');
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 在线文档有效' : '❌ 同上错误'}`);
1711
1721
  lines.push(' 4️⃣ doc metadata → ❌ 404(该端点不存在)');
1712
1722
  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('📌 请重新运行此工具,已加自动重试逻辑');
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');
1719
1732
 
1720
1733
  return { content: [{ type: 'text', text: lines.join('\n') }] };
1721
1734
  }
@@ -2085,9 +2098,74 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2085
2098
  };
2086
2099
  } catch (blocksErr) {
2087
2100
  const isDocKeyIllegal = blocksErr.message.includes('doc key is illegal');
2101
+
2102
+ // Fallback: 尝试存储 v1.0 下载 API(对上传的 .md 等文件有效)
2103
+ if (isDocKeyIllegal && workspaceId) {
2104
+ try {
2105
+ const token = await dingtalk.getAccessToken();
2106
+ const opId = await dingtalk.resolveOperatorId(operator_id || null);
2107
+
2108
+ // BFS 遍历查找 dentry
2109
+ const queue = [''];
2110
+ const visited = new Set();
2111
+ let foundDentry = null;
2112
+ let foundSpaceId = null;
2113
+ while (queue.length > 0 && !foundDentry) {
2114
+ const parentId = queue.shift();
2115
+ if (visited.has(parentId)) continue;
2116
+ visited.add(parentId);
2117
+ try {
2118
+ const dirParams = { operatorId: opId, maxResults: 500 };
2119
+ if (parentId) dirParams.dentryId = parentId;
2120
+ const dirRes = await axios({
2121
+ method: 'GET',
2122
+ url: `${DINGTALK_API_V2}/v2.0/doc/spaces/${workspaceId}/directories`,
2123
+ headers: { 'x-acs-dingtalk-access-token': token },
2124
+ params: dirParams
2125
+ });
2126
+ const children = dirRes.data?.children || [];
2127
+ for (const child of children) {
2128
+ if (child.dentryUuid === realDocKey || child.dentryId === realDocKey || child.id === realDocKey) {
2129
+ foundDentry = child;
2130
+ foundSpaceId = child.spaceId;
2131
+ break;
2132
+ }
2133
+ if (child.hasChildren && child.dentryId && !visited.has(child.dentryId)) {
2134
+ queue.push(child.dentryId);
2135
+ }
2136
+ }
2137
+ } catch (e) { /* skip */ }
2138
+ }
2139
+
2140
+ if (foundDentry && foundSpaceId) {
2141
+ const dlRes = await axios({
2142
+ method: 'POST',
2143
+ url: `${DINGTALK_API_V2}/v1.0/storage/spaces/${foundSpaceId}/dentries/${foundDentry.dentryId}/downloadInfos/query`,
2144
+ headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
2145
+ params: { unionId: opId },
2146
+ data: {}
2147
+ });
2148
+ const data = dlRes.data;
2149
+ const downloadUrl = data.downloadInfo?.resourceUrl || data.resourceUrl || data.downloadUrl || data.url;
2150
+ const downloadHeaders = data.downloadInfo?.headers || data.headers || {};
2151
+ if (downloadUrl) {
2152
+ const contentRes = await axios({ method: 'GET', url: downloadUrl, headers: downloadHeaders, responseType: 'text' });
2153
+ return {
2154
+ content: [{ type: 'text', text: contentRes.data }]
2155
+ };
2156
+ }
2157
+ }
2158
+ } catch (fallbackErr) {
2159
+ console.error(`[钉钉MCP] 存储 v1.0 fallback 失败: ${fallbackErr.message}`);
2160
+ }
2161
+ }
2162
+
2088
2163
  let hint = '';
2089
2164
  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)。`;
2165
+ hint = `\n\n原因: 此文档不是 .adoc 在线文档,blocks API 无法读取。`;
2166
+ if (workspaceId) {
2167
+ hint += `\n已尝试存储 v1.0 下载 API 但仍失败。`;
2168
+ }
2091
2169
  }
2092
2170
  return {
2093
2171
  content: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-wiki",
3
- "version": "1.2.15",
3
+ "version": "1.2.16",
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": {