Skill: shyzhen/dingtalk-doc-enterprise (version 1.0.6)

--- name: dingtalk-doc-enterprise description: 钉钉文档操作技能。仅在消息明确包含 `alidocs.dingtalk.com`、`钉钉文档`、`钉钉知识库`、`alidocs`，或当前上下文已明确对象是钉钉文档时使用。支持 read/blocks/update/append-text/delete 命令，不支持 create。 metadata: { "openclaw": { "emoji": "📄", "requires": { "bins": ["node"], "env": ["DINGTALK_CLIENTID", "DINGTALK_CLIENTSECRET"], }, }, } --- # 钉钉文档企业版 使用同目录脚本 `doc-enterprise.js` 调用钉钉文档企业 API。OpenClaw 不会因为目录里存在 `.js` 文件就自动执行它；当任务命中本 skill 时，应按下面规则主动调用脚本。 ## 何时使用 ### 🔑 触发关键词 只有在**已经确认对象是钉钉文档**时，同时消息包含以下**任一关键词**时，优先使用本 skill： | 类别 | 关键词 | |------|--------| | **平台名** | 钉钉文档、钉钉知识库、alidocs | | **读取类** | 总结、读取、查看、浏览、列出结构 | | **修改类** | 更新、修改、追加、删除、覆写 | | **对象** | 文档、链接、这篇、这个文档 | **组合示例：** - "总结一下这篇钉钉文档" - "读取这个 alidocs 链接" - "更新文档内容" - "删除第三段" ### ✅ 触发场景（优先级从高到低） | 场景 | 示例 | 动作 | |------|------|------| | **钉钉文档链接** | `alidocs.dingtalk.com/i/nodes/xxx` | 自动识别，调用 `read` 或 `blocks` | | **钉钉上下文 + 链接** | "总结 https://alidocs.dingtalk.com/..." | 根据意图选择命令 | | **明确命令** | "总结这篇文档"、"读取这个 alidocs 链接" | 根据意图选择命令 | | **已知上下文是钉钉文档** | 前文已给出 alidocs 链接，后续说"更新文档"、"删除某段" | 调用对应命令 | | **结构查询** | "列出结构"、"这个 alidocs 有哪些章节" | 调用 `blocks` | ### ❌ 不触发的场景 - 创建新文档（脚本未实现 `create`） - 没有文档链接、docKey、或明确钉钉文档上下文，却要求修改文档 - 用户只说“总结文档”“更新这个链接”等泛化请求，但上下文无法确认对象是钉钉文档 - 与钉钉无关的文档系统，例如本地 Markdown、飞书文档、语雀、Google Docs --- ## 🔑 链接识别规则 支持以下格式的钉钉文档链接： ``` https://alidocs.dingtalk.com/i/nodes/<docKey> https://alidocs.dingtalk.com/i/nodes/<docKey>?utm_scene=person_space https://alidocs.dingtalk.com/i/nodes/<docKey>?utm_scene=team_space alidocs.dingtalk.com/i/nodes/<docKey> （无 https） ``` 脚本会自动提取 `<docKey>` 部分。 ## 运行前提 必需环境变量： - `DINGTALK_CLIENTID` - `DINGTALK_CLIENTSECRET` 可选环境变量： - `DINGTALK_OPERATOR_ID`：默认操作人 unionId - `OPENCLAW_SENDER_ID`：由 OpenClaw/连接器注入，用于自动识别当前发送者 - `OPENCLAW_SENDER_NAME` 脚本优先使用 `DINGTALK_OPERATOR_ID`。若未设置，则尝试使用 `OPENCLAW_SENDER_ID` 查询 unionId。 ## 执行入口 脚本文件：`doc-enterprise.js` 执行时使用绝对路径，形式如下： ```bash node /absolute/path/to/doc-enterprise.js <command> [args] ``` 当本 skill 引用了相对路径文件时，将其相对于 `SKILL.md` 所在目录解析成绝对路径后再执行。 ## 命令映射 ### 1. 读取文档概览 适用： - 用户只说“读取这篇文档” - 用户贴了一个文档链接，想先快速看内容概况 执行： ```bash node /absolute/path/to/doc-enterprise.js read <docKey-or-url> ``` 说明： - `read` 只输出块列表和部分预览文本 - 适合快速探测，不适合直接拿来做高质量总结 ### 2. 获取完整结构或用于总结 适用： - 用户要求“总结这篇文档” - 用户要求“列出结构” - 后续操作需要 `blockId` 执行： ```bash node /absolute/path/to/doc-enterprise.js blocks <docKey-or-url> ``` 说明： - 优先用 `blocks` 获取更完整的块结构 - 做总结前，先读取 `blocks` 的结果，再基于结果总结 - 删除或追加前，如果用户没有给出 `blockId`，先运行 `blocks` ### 3. 覆写整篇文档 适用： - 用户明确要求“更新整篇文档” - 用户给出了新的 Markdown 内容 执行： ```bash node /absolute/path/to/doc-enterprise.js update <docKey-or-url> <markdown> ``` 说明： - 这是整篇覆写，不是局部编辑 - 若用户只是想在末尾补一段，不要用 `update` ### 4. 追加文本到段落块 适用： - 用户要求在某个已知段落块后追加文本 - 已经拿到了目标 `blockId` 执行： ```bash node /absolute/path/to/doc-enterprise.js append-text <docKey-or-url> <blockId> <text> ``` 说明： - 如果没有 `blockId`，先运行 `blocks` - 仅在目标块确实是段落块时使用 - “没有 `blockId`” 不代表不触发本 skill，而是先查结构再执行追加 ### 5. 删除块元素 适用： - 用户明确要求删除某一段或某个块 - 已经拿到了目标 `blockId` 执行： ```bash node /absolute/path/to/doc-enterprise.js delete <docKey-or-url> <blockId> ``` 说明： - 删除前先确认目标块，避免误删 - 如果用户说“删除第 3 段”，先运行 `blocks` 找到对应 `blockId` - “没有 `blockId`” 不代表不触发本 skill，而是先查结构再执行删除 ## 工作流程 1. 从用户消息中提取文档链接或 docKey。 2. 先确认对象确实是钉钉文档，再判断是读取、总结、列结构、覆写、追加，还是删除。 3. 需要 `blockId` 时，先运行 `blocks`。 4. 读取概览用 `read`；做总结或定位块时优先用 `blocks`。 5. 执行脚本后，把结果用自然语言返回给用户。 ## 错误处理 若脚本报以下错误，按对应方式处理： - 缺少 `DINGTALK_CLIENTID` 或 `DINGTALK_CLIENTSECRET`：提示未配置钉钉应用凭证 - 缺少 `operatorId`：提示配置 `DINGTALK_OPERATOR_ID`，或确认连接器是否传入 `OPENCLAW_SENDER_ID` - `forbidden.accessDenied`：提示当前用户对该文档无权限 - `docNotExist`：提示文档不存在，检查链接或 docKey - `blockNotExist`：提示块不存在，先重新运行 `blocks` 核对 - `paramError`：提示参数格式错误，优先检查链接与文档 ID ## 限制 - 当前脚本未实现创建文档，不要承诺或尝试 `create` - `read` 只适合概览，不足以替代完整正文提取 - 追加和删除依赖 `blockId` - 覆写会直接替换整篇文档内容 ## 参考 - 背景说明与人工阅读材料：`README.md` - 实际执行脚本：`doc-enterprise.js`

path: doc-enterprise.js
content: #!/usr/bin/env node
/**
 * 钉钉文档企业 API 工具
 *
 * 支持操作：读取文档概览、查询块结构、插入块、覆写内容、删除块、追加段落文本
 *
 * 环境变量：
 *   DINGTALK_CLIENTID - 企业内部应用 ClientId
 *   DINGTALK_CLIENTSECRET - 企业内部应用 ClientSecret
 *   DINGTALK_OPERATOR_ID - 操作人 unionId（可选，不设置则自动获取）
 */

const API_BASE = 'https://api.dingtalk.com';
const TIMEOUT_MS = 30000;

// ============ 配置 ============

const APP_KEY = process.env.DINGTALK_CLIENTID;
const APP_SECRET = process.env.DINGTALK_CLIENTSECRET;
const OPERATOR_ID = process.env.DINGTALK_OPERATOR_ID;

// 用户缓存（避免重复查询）
const userCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟

// ============ 工具函数 ============

/**
 * 获取 Access Token
 */
async function getAccessToken() {
  if (!APP_KEY || !APP_SECRET) {
    throw new Error('缺少配置：请设置 DINGTALK_CLIENTID 和 DINGTALK_CLIENTSECRET');
  }

  const response = await fetch(`${API_BASE}/v1.0/oauth2/accessToken`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ appKey: APP_KEY, appSecret: APP_SECRET })
  });

  if (!response.ok) throw new Error(`获取 Token 失败：HTTP ${response.status}`);

  const data = await response.json();
  if (data.code && data.code !== 0) throw new Error(`获取 Token 失败：${data.message}`);

  return data.accessToken;
}

/**
 * 通过 userId 获取 unionId（operatorId）
 */
async function getUnionId(userId, token) {
  // 检查缓存
  const cached = userCache.get(userId);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.unionId;
  }

  // 使用旧版 API（新版 topapi/v2 不存在）
  const response = await fetch(`https://oapi.dingtalk.com/user/get?access_token=${token}&userid=${userId}`, {
    method: 'GET'
  });

  if (!response.ok) throw new Error(`获取用户信息失败：HTTP ${response.status}`);

  const data = await response.json();
  if (data.errcode !== 0) throw new Error(`获取用户信息失败：${data.errmsg}`);

  const unionId = data.unionid;
  userCache.set(userId, { unionId, timestamp: Date.now() });

  return unionId;
}

/**
 * 获取当前用户的 operatorId
 *
 * 优先级：
 * 1. 环境变量 DINGTALK_OPERATOR_ID
 * 2. 从 OpenClaw 环境变量获取 sender_id，然后查询 unionId
 */
async function getCurrentOperatorId() {
  // 优先级 1：环境变量
  if (OPERATOR_ID) {
    console.log(`[用户识别] 使用环境变量：${OPERATOR_ID}`);
    return OPERATOR_ID;
  }

  // 优先级 2：从 OpenClaw 环境变量获取 sender_id
  const senderId = process.env.OPENCLAW_SENDER_ID || process.env.DINGTALK_SENDER_ID;
  if (senderId) {
    const token = await getAccessToken();
    const unionId = await getUnionId(senderId, token);
    console.log(`[用户识别] 从 sender_id 获取：${unionId}`);
    return unionId;
  }

  throw new Error('缺少 operatorId：请配置 DINGTALK_OPERATOR_ID 环境变量，或确保钉钉连接器传递了 sender_id');
}

/**
 * 调用企业 API
 */
async function callAPI(endpoint, method = 'GET', body = null, operatorId, token) {
  if (!operatorId) operatorId = await getCurrentOperatorId();
  if (!token) token = await getAccessToken();

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);

  const separator = endpoint.includes('?') ? '&' : '?';
  const url = `${endpoint}${separator}operatorId=${encodeURIComponent(operatorId)}`;

  const options = {
    method,
    headers: {
      'x-acs-dingtalk-access-token': token,
      'Content-Type': 'application/json'
    },
    signal: controller.signal
  };

  if (body) options.body = JSON.stringify(body);

  try {
    const response = await fetch(`${API_BASE}${url}`, options);
    clearTimeout(timeoutId);

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(`API 调用失败：HTTP ${response.status} - ${errorData.message || errorData.code || response.statusText}`);
    }

    const result = await response.json();
    if (result.code && result.code !== 0) throw new Error(result.message || JSON.stringify(result));

    return result;
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

/**
 * 从 URL 提取文档 ID
 */
function extractDocId(input) {
  if (!input) return null;
  if (!input.includes('http') && !input.includes('/')) return input;

  const patterns = [
    /\/i\/nodes\/([a-zA-Z0-9]+)/,
    /\/nodes\/([a-zA-Z0-9]+)/,
    /docKey=([a-zA-Z0-9]+)/,
    /dentryKey=([a-zA-Z0-9]+)/,
  ];

  for (const pattern of patterns) {
    const match = input.match(pattern);
    if (match && match[1]) return match[1];
  }

  return input;
}

function truncateText(text, maxLength = 80) {
  if (!text) return '';
  return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
}

function blockPreview(block) {
  if (block?.paragraph?.text) return truncateText(block.paragraph.text, 80);
  if (Array.isArray(block?.paragraph?.contents)) {
    const text = block.paragraph.contents.map(item => item?.text || '').join('').trim();
    if (text) return truncateText(text, 80);
  }
  if (block?.heading?.text) return truncateText(block.heading.text, 80);
  return '';
}

// ============ API 封装 ============

async function queryBlocks(dentryUuid, operatorId, token) {
  console.log(`[查询块元素] ${dentryUuid}`);
  return callAPI(`/v1.0/doc/suites/documents/${dentryUuid}/blocks`, 'GET', null, operatorId, token);
}

async function insertBlock(dentryUuid, element, position, operatorId, token) {
  console.log(`[插入块元素] ${dentryUuid}`);
  const body = { element };
  if (position) body.position = position;
  return callAPI(`/v1.0/doc/suites/documents/${dentryUuid}/blocks`, 'POST', body, operatorId, token);
}

async function modifyBlock(dentryUuid, blockId, element, operatorId, token) {
  console.log(`[更新块元素] ${dentryUuid}/${blockId}`);
  return callAPI(`/v1.0/doc/suites/documents/${dentryUuid}/blocks/${blockId}`, 'PUT', { element }, operatorId, token);
}

async function deleteBlock(dentryUuid, blockId, operatorId, token) {
  console.log(`[删除块元素] ${dentryUuid}/${blockId}`);
  return callAPI(`/v1.0/doc/suites/documents/${dentryUuid}/blocks/${blockId}`, 'DELETE', null, operatorId, token);
}

async function appendText(dentryUuid, blockId, text, operatorId, token) {
  console.log(`[追加文本] ${dentryUuid}/${blockId}`);
  return callAPI(`/v1.0/doc/suites/documents/${dentryUuid}/paragraphs/${blockId}/text`, 'POST', { text }, operatorId, token);
}

async function overwriteContent(docKey, markdown, operatorId, token) {
  console.log(`[覆写文档] ${docKey}`);
  return callAPI(`/v1.0/doc/suites/documents/${docKey}/overwriteContent`, 'POST', { dataType: 'markdown', content: markdown }, operatorId, token);
}

// ============ 命令处理 ============

async function cmdRead(args) {
  const docKey = extractDocId(args[0]);
  if (!docKey) { console.error('错误：请提供文档 ID 或链接'); process.exit(1); }

  const operatorId = await getCurrentOperatorId();
  const token = await getAccessToken();
  const result = await queryBlocks(docKey, operatorId, token);

  console.log('\n=== 文档概览 ===\n');
  if (result.blocks && result.blocks.length > 0) {
    result.blocks.forEach((block, index) => {
      console.log(`${index + 1}. [${block.blockType}] ${block.blockId}`);
      const preview = blockPreview(block);
      if (preview) console.log(`   预览：${preview}`);
    });
    console.log('\n提示：如果要做总结、定位 blockId 或查看完整结构，请使用 blocks 命令。');
  } else {
    console.log('(空文档)');
  }
}

async function cmdBlocks(args) {
  const dentryUuid = extractDocId(args[0]);
  if (!dentryUuid) { console.error('错误：请提供文档 ID 或链接'); process.exit(1); }

  const operatorId = await getCurrentOperatorId();
  const token = await getAccessToken();
  const result = await queryBlocks(dentryUuid, operatorId, token);

  console.log('\n=== 块元素详情 ===\n');
  console.log(JSON.stringify(result, null, 2));
}

async function cmdInsert(args) {
  const dentryUuid = extractDocId(args[0]);
  const position = args[1];
  const content = args.slice(2).join(' ');
  if (!dentryUuid || !content) { console.error('错误：请提供文档 ID、位置和內容'); process.exit(1); }

  const operatorId = await getCurrentOperatorId();
  const token = await getAccessToken();
  const result = await insertBlock(dentryUuid, { blockType: 'paragraph', paragraph: { text: content } }, position, operatorId, token);
  console.log('✅ 插入成功');
  console.log(JSON.stringify(result, null, 2));
}

async function cmdDelete(args) {
  const dentryUuid = extractDocId(args[0]);
  const blockId = args[1];
  if (!dentryUuid || !blockId) { console.error('错误：请提供文档 ID 和块 ID'); process.exit(1); }

  const operatorId = await getCurrentOperatorId();
  const token = await getAccessToken();
  const result = await deleteBlock(dentryUuid, blockId, operatorId, token);
  console.log('✅ 删除成功');
  console.log(JSON.stringify(result, null, 2));
}

async function cmdAppendText(args) {
  const dentryUuid = extractDocId(args[0]);
  const blockId = args[1];
  const text = args.slice(2).join(' ');
  if (!dentryUuid || !blockId || !text) { console.error('错误：请提供文档 ID、块 ID 和文本'); process.exit(1); }

  const operatorId = await getCurrentOperatorId();
  const token = await getAccessToken();
  const result = await appendText(dentryUuid, blockId, text, operatorId, token);
  console.log('✅ 追加成功');
  console.log(JSON.stringify(result, null, 2));
}

async function cmdUpdate(args) {
  const docKey = extractDocId(args[0]);
  const markdown = args.slice(1).join(' ');
  if (!docKey || !markdown) { console.error('错误：请提供文档 ID 和内容'); process.exit(1); }

  const operatorId = await getCurrentOperatorId();
  const token = await getAccessToken();
  const result = await overwriteContent(docKey, markdown, operatorId, token);
  console.log('✅ 更新成功');
  console.log(JSON.stringify(result, null, 2));
}

// ============ 主函数 ============

async function main() {
  const args = process.argv.slice(2);

  if (!args.length || args.includes('--help')) {
    console.log(`
钉钉文档企业 API 工具

用法:
  node doc-enterprise.js <command> [args]

命令:
  read <docKey|url>           读取文档概览（块列表 + 预览）
  blocks <docKey|url>         查询完整块结构（适合总结和定位 blockId）
  insert <docKey|url> <pos> <content>  插入块元素
  delete <docKey|url> <blockId>       删除块元素
  append-text <docKey|url> <blockId> <text>  追加文本到段落
  update <docKey|url> <markdown>      覆写整篇文档内容

说明:
  当前脚本未实现 create。
  如需总结文档或查找 blockId，请优先使用 blocks。

示例:
  node doc-enterprise.js read https://alidocs.dingtalk.com/i/nodes/xxx
  node doc-enterprise.js update xxx "# 标题\\n\\n内容"

环境变量:
  DINGTALK_CLIENTID          企业内部应用 ClientId
  DINGTALK_CLIENTSECRET      企业内部应用 ClientSecret
  DINGTALK_OPERATOR_ID       默认操作人 unionId（可选）
  OPENCLAW_SENDER_ID         钉钉连接器传递的 sender_id（自动注入）
`);
    process.exit(0);
  }

  const command = args[0];
  const commandArgs = args.slice(1);

  try {
    switch (command) {
      case 'read': await cmdRead(commandArgs); break;
      case 'blocks': await cmdBlocks(commandArgs); break;
      case 'insert': await cmdInsert(commandArgs); break;
      case 'delete': await cmdDelete(commandArgs); break;
      case 'append-text': await cmdAppendText(commandArgs); break;
      case 'update': await cmdUpdate(commandArgs); break;
      default: console.error(`未知命令：${command}`); process.exit(1);
    }
  } catch (error) {
    console.error('\n错误:', error.message);
    if (error.message.includes('operatorId')) {
      console.error('\n💡 提示：配置 DINGTALK_OPERATOR_ID 或确保钉钉连接器传递了 sender_id');
    } else if (error.message.includes('CLIENTID')) {
      console.error('\n💡 提示：配置 DINGTALK_CLIENTID 和 DINGTALK_CLIENTSECRET');
    } else if (error.message.includes('403')) {
      console.error('\n💡 提示：检查应用权限（Storage.File.Read/Write）');
    }
    process.exit(1);
  }
}

main();


path: README.md
content: # 钉钉文档企业版技能 (dingtalk-doc-enterprise)

通过钉钉开放平台企业 API 管理钉钉文档，支持多用户场景，自动从钉钉连接器获取当前用户身份。

> 背景：我在使用openclaw和钉钉的官方连接器，但是他们不支持这些功能（最新版本0.8.13）。
>
> 这是我的issue： https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues/456
>
> 网上找遍了资源，也加入了钉钉开发群，都说不支持、还说没有操作文档的API。
>
> 连接器的文档说["钉钉文档能力依赖 MCP（Model Context Protocol）提供底层 tool"](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector?tab=readme-ov-file#%E9%92%89%E9%92%89%E6%96%87%E6%A1%A3docs%E4%B8%8E-mcpdocs) ，这种方案全是权限问题。
>
> [于是有了这个玩具。](https://clawhub.ai/shyzhen/dingtalk-doc-enterprise)

---

## 🚀 快速开始

### 1. 配置钉钉凭证

**推荐方式：编辑 `~/.openclaw/.env` 文件**

OpenClaw 会在启动时自动加载此文件中的环境变量，无需手动设置。

```bash
# ~/.openclaw/.env
DINGTALK_CLIENTID=dingxxxxxx
DINGTALK_CLIENTSECRET=your_secret
```

**获取凭证：**
1. 登录 [钉钉开放平台](https://open.dingtalk.com/)
2. 进入「应用开发」→「企业内部开发」
3. 创建或选择已有应用
4. 在「应用详情」页面获取 `AppKey` (ClientId) 和 `AppSecret` (ClientSecret)

**注意：** 修改 `.env` 文件后需要重启 OpenClaw Gateway 才能生效。

---

### 2. 申请应用权限

在钉钉开放平台「权限管理」中申请以下权限：

| 权限码 | 用途 | 必需 |
|--------|------|------|
| `qyapi_get_member` | 获取用户信息 | ✅ |
| `Storage.File.Read` | 读取文档内容 | ✅ |
| `Storage.File.Write` | 编辑/创建文档 | ✅ |

**申请步骤：**
1. 进入应用管理页面
2. 点击「权限管理」
3. 搜索上述权限码并申请
4. 等待管理员审批通过

### 3. 验证配置

重启 OpenClaw Gateway 后，可以通过以下方式验证配置是否生效：

```bash
# 检查环境变量是否加载
openclaw env | Select-String DINGTALK

# 或查看 Gateway 日志
openclaw logs --grep DINGTALK
```

---

## ✨ 核心特性

- ✅ **自动身份识别** - 从钉钉连接器消息自动获取发送者身份
- ✅ **多用户支持** - 企业内所有用户均可使用
- ✅ **权限隔离** - 每个用户只能操作自己有权限的文档
- ✅ **文档读取与修改** - 支持读取概览、查询块结构、覆写整篇内容、追加文本、删除块元素

---

## 📖 使用方式

### 自动触发（推荐）

**发送钉钉文档链接即可自动读取：**
```
https://alidocs.dingtalk.com/i/nodes/xxx
```

**文档链接 + 操作指令：**
```
帮我总结这篇文档：https://alidocs.dingtalk.com/i/nodes/xxx
在这篇文档里添加一段测试数据：https://alidocs.dingtalk.com/i/nodes/xxx
```

### 指令触发

| 操作 | 指令示例 |
|------|---------|
| 读取文档 | `读取钉钉文档 <链接>` |
| 总结文档 | `总结这篇文档：<链接>` |
| 更新文档 | `更新文档 <链接> 内容为...` |
| 追加内容 | `在文档指定段落后追加：<链接> 内容...` |
| 删除块元素 | `删除文档第 X 段：<链接>` |
| 查询结构 | `列出文档结构：<链接>` |

---

## 🔧 工作原理

```
用户发送消息 
    ↓
钉钉连接器接收
    ↓
OpenClaw 提取 sender_id
    ↓
查询用户 unionId
    ↓
作为 operatorId 调用钉钉 API
    ↓
返回结果给用户
```

---

## ⚠️ 注意事项

### 权限说明
- 每个用户只能操作**自己有权限访问**的文档
- 企业公开文档对所有用户可见
- 私有文档需要单独授权

### 限制
- 当前版本**不支持创建新文档**
- `read` 更适合快速概览；如果要总结文档或定位块，请先使用 `blocks`
- 追加和删除操作依赖 `blockId`，通常需要先查询结构
- 单次内容最大长度：50000 字符
- 频率限制：100 次/分钟/文档
- 块元素更新：目前仅支持段落类型

---

## 🐛 常见问题

| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `forbidden.accessDenied` | 无文档权限 | 确认用户对文档有访问权限 |
| `docNotExist` | 文档不存在 | 检查文档 ID 是否正确 |
| `operatorId invalid` | 用户身份无效 | 检查钉钉连接器配置 |
| `paramError` | 参数错误 | 检查文档链接格式 |
| 缺少 `DINGTALK_CLIENTID` / `DINGTALK_CLIENTSECRET` | 未配置应用凭证 | 在 `~/.openclaw/.env` 中补齐并重启 Gateway |

---

## 📚 CLI 参考

脚本文件为同目录下的 `doc-enterprise.js`，常用命令如下：

```bash
node doc-enterprise.js read <docKey|url>
node doc-enterprise.js blocks <docKey|url>
node doc-enterprise.js update <docKey|url> "<markdown>"
node doc-enterprise.js append-text <docKey|url> <blockId> "<text>"
node doc-enterprise.js delete <docKey|url> <blockId>
```

- `read`：输出块列表和文本预览，适合快速查看
- `blocks`：输出完整块结构，适合总结文档或定位 `blockId`
- `update`：整篇覆写 Markdown 内容
- `append-text` / `delete`：需要已知 `blockId`

## 📚 API 参考

支持的钉钉文档 API 端点：

| 操作 | 端点 | 方法 |
|------|------|------|
| 覆写文档 | `/v1.0/doc/suites/documents/{docKey}/overwriteContent` | POST |
| 查询块元素 | `/v1.0/doc/suites/documents/{dentryUuid}/blocks` | GET |
| 插入块元素 | `/v1.0/doc/suites/documents/{dentryUuid}/blocks` | POST |
| 更新块元素 | `/v1.0/doc/suites/documents/{dentryUuid}/blocks/{blockId}` | PUT |
| 删除块元素 | `/v1.0/doc/suites/documents/{dentryUuid}/blocks/{blockId}` | DELETE |

---

**最后更新：** 2026-04-10  
**版本：** 1.0  
**作者：** Ash · ShyZhen


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)